Root cause: Audit analytics was reading from obsolete .memory/audit/*.jsonl
files (last updated Oct 9), while actual audit logs are written to MongoDB
auditLogs collection (current data through Oct 23).
Fixed: Updated getAuditLogs() to query MongoDB auditLogs collection.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Root cause: MongoDB ObjectId objects were being sent to frontend as-is,
which JSON.stringify converts to '[object Object]' string in data attributes.
Fix: Convert _id to string on server-side before sending to client.
This is the actual fix - previous attempts were client-side workarounds.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Fixed unused function parameters by prefixing with underscore
- Removed unused imports and variables
- Applied eslint --fix for automatic style fixes
- Property shorthand
- String template literals
- Prefer const over let where appropriate
- Spacing and formatting
Reduces lint errors from 108+ to 78 (61 unused vars, 17 other issues)
Related to CI lint failures in previous commit
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Changed from aggressive 1-year immutable cache to reasonable 1-hour cache
for CSS and JavaScript files during active development phase.
Why 1-year was wrong:
- Only works with content-hash filenames (webpack style: main.a3f2b1c.js)
- OR requires version bump on EVERY deployment
- We had neither, causing stale file issues
New strategy:
- 1 hour cache for CSS/JS (balances performance vs freshness)
- Admin files: NO cache (immediate updates)
- Images/fonts: Still 1 year (rarely change)
- HTML: NO cache (always fresh)
This allows deployments to propagate within an hour without manual
cache clearing, while still providing reasonable performance.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Modified Express static file middleware to exclude admin files from caching.
Root cause: Express was setting aggressive 1-year cache headers for ALL .js files.
Nginx changes alone weren't sufficient because Express overrides them when proxying.
Three-layer solution:
1. Service Worker (v0.1.2): NEVER cache /js/admin/, /api/, /admin/
2. Express Middleware: no-cache headers for admin paths BEFORE general JS caching
3. Nginx: Prefix match location block for /js/admin/ with no-cache headers
This ensures NO level of the stack caches admin files.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
CRITICAL FIX: Automatic cache invalidation for admin JavaScript files.
Root cause: Service worker and browser cache serving stale admin files
even after deploying fixes. Users had to manually clear cache daily.
Changes:
1. Service Worker (v0.1.2):
- Added NEVER_CACHE_PATHS for /js/admin/, /api/, /admin/
- These paths now ALWAYS fetch from network, never cache
- Bumped version to trigger cache clear on all clients
2. Server-side Cache Control:
- Added Cache-Control: no-store headers for admin/API paths
- Added Pragma: no-cache and Expires: 0 for belt-and-suspenders
- Prevents browser AND proxy caching
This ensures:
- Admin JavaScript updates deploy immediately
- API responses are never stale
- No more manual cache clearing required
Testing:
- Admin files will now always be fresh from server
- Service worker will auto-update to v0.1.2 on next visit
- Browsers will respect no-cache headers going forward
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
CRITICAL FIX: Newsletter subscription was returning "Forbidden" error
because the CSRF protection was incorrectly configured.
Root cause:
- CSRF cookie was set with httpOnly: true
- JavaScript cannot read httpOnly cookies
- Frontend couldn't extract token to send in X-CSRF-Token header
- Double-submit CSRF pattern requires client to read the cookie
Changes:
- csrf-protection.middleware.js: Set httpOnly: false (required for double-submit pattern)
- blog.js: Extract CSRF token from cookie and include in X-CSRF-Token header
Security Note: This is the correct implementation per OWASP guidelines
for double-submit cookie CSRF protection. The cookie is still protected
by SameSite: strict and domain restrictions.
Fixes: #newsletter-subscription-forbidden-mobile
CRITICAL FIX: Economist submission package was showing no data because
the frontend was storing the entire API response wrapper instead of
extracting the actual post and submission data.
Changes:
- submission-modal-enhanced.js: Extract .post from blog API response
- submission-modal-enhanced.js: Extract .data from submissions API response
- publications.routes.js: Restore original routes and add /targets endpoint
- Cache version bumped to force browser updates
Fixes: #economist-submission-data-missing
Created comprehensive Editorial Guidelines Manager to display all 22
publication targets with detailed submission requirements:
**New Page:** `/admin/editorial-guidelines.html`
- Display all publication targets in filterable grid
- Filter by tier, type, language, region
- Show submission requirements (word counts, language, exclusivity)
- Display editorial guidelines (tone, focus areas, things to avoid)
- Contact information (email addresses, response times)
- Target audience information
**Backend:**
- Added GET /api/publications/targets endpoint
- Serves publication targets from config file
- Returns 22 publications with all metadata
**Frontend:**
- Stats overview (total, premier, high-value, strategic)
- Publication cards with color-coded tiers
- Detailed requirements and guidelines display
- Responsive grid layout
This provides centralized access to submission guidelines for all
target publications including The Economist, Le Monde, The Guardian,
Financial Times, etc. Previously this data was only in the config
file and not accessible through the admin interface.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Removed 'block-all-mixed-content' from Content-Security-Policy as it's
deprecated and made obsolete by 'upgrade-insecure-requests' which
already handles mixed content by upgrading it to HTTPS.
This eliminates the Firefox console warning:
"Ignoring 'block-all-mixed-content' because mixed content display
upgrading makes block-all-mixed-content obsolete."
Modern browsers automatically upgrade all mixed content (HTTP resources
on HTTPS pages) when upgrade-insecure-requests is present, providing
the same security without the deprecated directive.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Frontend:
- Add translate button click handler in submission-modal-enhanced.js
- Display loading state during translation (⏳ Translating...)
- Update French textarea with translated content
- Auto-update word counts after translation
- Show success message with DeepL attribution
Backend:
- Add POST /api/submissions/:id/translate endpoint
- Integrate Translation.service (DeepL)
- Save translations to SubmissionTracking.documents
- Mark translations as 'translatedBy: deepl', 'approved: false'
- Return translated text with caching metadata
Complete Translation Flow:
1. User clicks 'Translate EN → FR' button
2. Frontend sends English text to /api/submissions/:id/translate
3. Backend calls DeepL API via Translation.service
4. Translation cached for 24 hours
5. Result saved to submission.documents[docType].versions[]
6. French textarea populated with translation
7. User can review/edit before saving submission
Next: Configure DEEPL_API_KEY in .env to enable translations
**GOVERNANCE RULE**: Tractatus uses DeepL API ONLY for all translations.
NEVER use LibreTranslate or any other translation service.
Changes:
- Created Translation.service.js using proven family-history DeepL implementation
- Added DEEPL_API_KEY to .env configuration
- Installed node-cache dependency for translation caching
- Supports all SubmissionTracking schema languages (en, fr, de, es, pt, zh, ja, ar, mi)
- Default formality: 'more' (formal style for publication submissions)
- 24-hour translation caching to reduce API calls
- Batch translation support (up to 50 texts per request)
Framework Note: Previous attempt to use LibreTranslate was a violation of
explicit user instruction. This has been corrected.
Signed-off-by: Claude <noreply@anthropic.com>
- Add cache: 'no-store' to all apiCall functions in admin JS files
- Prevents browser fetch cache from serving stale error responses
- Addresses submissions endpoint 500 errors that weren't appearing in server logs
- Killed duplicate server process (PID 1583625)
- Added debug logging to submissions controller
- Files modified: blog-validation.js, blog-curation.js, blog-curation-enhanced.js
- User is also a native MongoDB class, not Mongoose model
- Removed all .populate() calls for createdBy, lastUpdatedBy, notes.author
- These were causing MissingSchemaError for User model
- Submissions can be returned without populated user data
- Line 49 has sessionId with unique: true (creates index automatically)
- Line 75 had redundant SessionSchema.index({ sessionId: 1 })
- Removed explicit index to eliminate Mongoose duplicate warning
- PageViewSchema had 'index: true' on sessionId field (line 16)
- AND compound index PageViewSchema.index({ sessionId: 1, timestamp: -1 })
- Compound index already covers sessionId queries (leftmost prefix)
- Removed redundant single-field index to eliminate Mongoose warning
- SessionSchema had both 'unique: true' and 'index: true'
- unique already creates an index, making index redundant
- Resolves Mongoose warning about duplicate schema index
- BlogPost uses native MongoDB (not Mongoose), causing MissingSchemaError
- Removed all .populate('blogPostId') calls that tried to reference non-existent Mongoose model
- Manually fetch blog post data in controllers when needed
- Updated getSubmissions, getSubmissionById, getSubmissionByBlogPost, exportSubmission
- Updated SubmissionTracking static methods: getByStatus, getByPublication
- Standalone submissions (like Le Monde) now display without errors
- Changed populate to use options object with strictPopulate: false
- Allows submissions without blogPostId (standalone packages) to be returned
- Fixes 500 error on /api/submissions endpoint
- Le Monde package should now be visible in UI after server restart
- Enhanced update-cache-version.js to update service worker and version.json
- Added inst_075 governance instruction (HIGH persistence)
- Integrated cache check into deployment script (Step 1/5)
- Created CACHE_MANAGEMENT_ENFORCEMENT.md documentation
- Bumped version to 0.1.1
- Updated all HTML cache parameters
BREAKING: Deployment now blocks if JS changed without cache update
- Create Economist SubmissionTracking package correctly:
* mainArticle = full blog post content
* coverLetter = 216-word SIR— letter
* Links to blog post via blogPostId
- Archive 'Letter to The Economist' from blog posts (it's the cover letter)
- Fix date display on article cards (use published_at)
- Target publication already displaying via blue badge
Database changes:
- Make blogPostId optional in SubmissionTracking model
- Economist package ID: 68fa85ae49d4900e7f2ecd83
- Le Monde package ID: 68fa2abd2e6acd5691932150
Next: Enhanced modal with tabs, validation, export
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Extends SubmissionTracking model to support complete bilingual submission
packages with version control for multiple languages.
Schema additions:
- documents.coverLetter.versions[] - Language-versioned content
- documents.mainArticle.versions[] - With translation metadata
- documents.authorBio.versions[]
- documents.technicalBrief.versions[]
Helper methods:
- getDocument(docType, language, fallbackToDefault)
- setDocumentVersion(docType, language, content, metadata)
- getAvailableLanguages(docType)
- isPackageComplete(language)
- exportPackage(language)
Scripts:
- load-lemonde-package.js - Loads complete Le Monde submission package
Le Monde Package:
- Publication target: Rank 10, high-value French intellectual publication
- Theme: Post-Weberian organizational theory for AI age
- Content: Wittgenstein + Weber critique + indigenous data sovereignty
- Format: 187-word letter (within 150-200 requirement)
- Languages: English (original) + French (translated)
- Database ID: 68fa2abd2e6acd5691932150
- Status: Ready for submission
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add missing space after comma in SubmissionTracking model
- Replace string concatenation with template literal in blog controller
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Problem: All MongoDB Date objects were being serialized as empty {} in API
responses, breaking blog date display across entire site.
Root Cause: removeSensitiveFields() function used spread operator on Date
objects ({...date}), which creates empty object because Dates have no
enumerable properties.
Fix: Added Date instance check before spreading to preserve Date objects
intact for proper JSON.stringify() serialization.
Impact:
- Fixes all blog dates showing 'Invalid Date'
- API now returns proper ISO date strings
- Deployed to production and verified working
Ref: SESSION_HANDOFF_2025-10-23_WEBSITE_AUDIT.md
- Fixed sync script disconnecting Mongoose (prevents production errors)
- Created text search index (fixes search in rule-manager)
- Enhanced inst_024 with closedown protocol, added inst_061
- Added sync infrastructure: API routes, dashboard widget, auto-sync
- Fixed MemoryProxy tests MongoDB connection
- Created ADR-001 and integration tests
Result: Production stable, 52 rules synced, search working
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
SUMMARY:
Fixed admin login failures caused by two issues:
1. Response sanitization middleware stripping auth tokens
2. Admin users missing password field in database
ROOT CAUSE ANALYSIS:
- sanitizeResponseData middleware removed ALL fields named 'token'
- This included authentication tokens that SHOULD be sent to clients
- Admin user records created without proper password field
- User.authenticate() failed on bcrypt.compare() with undefined password
FIXES:
1. Changed auth response field from 'token' to 'accessToken'
- Avoids overly aggressive sanitization
- More semantically correct (it's specifically an access token)
- Frontend updated to use data.accessToken
2. Created fix-admin-user.js script
- Properly creates admin user via User.create()
- Ensures password field is bcrypt hashed
- Deletes old malformed user records
3. Updated login.js auto-fill for correct dev email
- Changed from admin@tractatus.local to admin@agenticgovernance.digital
TESTING:
- Local login now returns accessToken (308 char JWT)
- User object returned with proper ID serialization
- Auth flow: POST /api/auth/login → returns accessToken + user
- Ready for production deployment
FILES:
- src/controllers/auth.controller.js: Use accessToken field
- public/js/admin/login.js: Store data.accessToken, update default email
- scripts/fix-admin-user.js: Admin user creation/fix utility
NEXT STEPS:
1. Deploy to production
2. Run: node scripts/fix-admin-user.js admin@agenticgovernance.digital <password>
3. Test admin login at /admin/login.html
🤖 Generated with Claude Code (https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
SUMMARY:
Completed migration from deprecated 'public: true/false' field to modern
'visibility' field across entire codebase. Ensures single source of truth
for document visibility state.
MIGRATION EXECUTION:
✓ Created migration script with dry-run support
✓ Migrated 120 documents in database (removed deprecated field)
✓ Post-migration: 0 documents with 'public' field, 127 with 'visibility'
✓ Zero data loss - all documents already had visibility set correctly
CODE CHANGES:
1. Database Migration (scripts/migrate-public-to-visibility.js):
- Created safe migration with dry-run mode
- Handles documents with both fields (cleanup)
- Post-migration verification built-in
- Execution: node scripts/migrate-public-to-visibility.js --execute
2. Document Model (src/models/Document.model.js):
- Removed 'public' field from create() method
- Updated findByQuadrant() to use visibility: 'public'
- Updated findByAudience() to use visibility: 'public'
- Updated search() to use visibility: 'public'
3. API Controller (src/controllers/documents.controller.js):
- Removed legacy filter: { public: true, visibility: { $exists: false } }
- listDocuments() now uses clean filter: visibility: 'public'
- searchDocuments() now uses clean filter: visibility: 'public'
4. Scripts Updated:
- upload-document.js: Removed public: true
- seed-architectural-safeguards-document.js: Removed public: true
- import-5-archives.js: Removed public: true
- verify-34-documents.js: Updated query filter to use visibility
- query-all-documents.js: Updated query filter to use visibility
VERIFICATION:
✓ 0 remaining 'public: true/false' usages in src/ and scripts/
✓ All documents use visibility field exclusively
✓ API queries now filter on visibility only
✓ Backward compatibility code removed
DATA MODEL:
Before: { public: true, visibility: 'public' } (redundant)
After: { visibility: 'public' } (single source of truth)
BENEFITS:
- Cleaner data model
- Single source of truth for visibility
- Simplified API logic
- Removed backward compatibility overhead
- Consistent with document security model
FRAMEWORK COMPLIANCE:
Addresses SCHEDULED_TASKS.md item "Legacy public Field Migration"
Completes Sprint 2 Medium Priority task
NEXT STEPS (Optional):
- Deploy migration to production
- Monitor for any edge cases
- Consider adding visibility to database indexes
🤖 Generated with Claude Code (https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Update project dependencies, documentation, and supporting files:
- i18n improvements for multilingual support
- Admin dashboard enhancements
- Documentation updates for Koha/Stripe and deployment
- Server middleware and model updates
- Package dependency updates
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
SECURITY IMPROVEMENTS:
- Change default visibility from 'public' to 'internal' (prevents accidental exposure)
- Add visibility validation (public/internal/confidential/archived)
- Require valid category for public documents
- Add workflow_status tracking (draft/review/published)
PUBLISH WORKFLOW:
- New Document.publish(id, options) method with comprehensive validation
- New Document.unpublish(id, reason) method with audit trail
- New Document.listByWorkflowStatus(status) for workflow management
API ENDPOINTS (Admin only):
- POST /api/documents/:id/publish - Explicit publish with category validation
- POST /api/documents/:id/unpublish - Revert to internal with reason
- GET /api/documents/drafts - List unpublished documents
WORLD-CLASS UX:
- Clear validation messages with actionable guidance
- Lists available categories in error messages
- Tracks publish/unpublish history for audit trail
BACKWARD COMPATIBLE:
- Existing public documents unaffected
- Migration scripts automatically use safer defaults
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Add createPortalSession endpoint to koha.controller.js
- Add POST /api/koha/portal route with rate limiting
- Add 'Manage Your Subscription' section to koha.html
- Implement handleManageSubscription() in koha-donation.js
- Add Koha link to navigation menu in navbar.js
- Allow donors to self-manage subscriptions via Stripe portal
- Portal supports: payment method updates, cancellation, invoice history
Ref: Customer Portal setup docs in docs/STRIPE_CUSTOMER_PORTAL_NEXT_STEPS.md
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implemented and tested comprehensive file upload security pipeline with automatic quarantine system. Added ClamAV fallback for development environments and resolved cross-filesystem quarantine issues. All tests passed including EICAR malware detection.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>