feat(cache): enforce mandatory cache version updates for JS changes

- 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
This commit is contained in:
TheFlow 2025-10-24 09:43:20 +13:00
parent ac2db33732
commit 782c90b2e7
30 changed files with 1697 additions and 179 deletions

View file

@ -2470,7 +2470,7 @@
"id": "inst_075",
"text": "AFTER each response, check <system-warning> for current token count. IF token count > next_checkpoint value in .claude/token-checkpoints.json, MUST run: node scripts/check-token-checkpoint.js --tokens [current]/[budget]. This generates pressure report and marks checkpoint as completed. Checkpoints are at 25% (50k), 50% (100k), 75% (150k). Checking checkpoints is MANDATORY, not optional. Token budget awareness prevents context window exhaustion and maintains quality.",
"timestamp": "2025-10-22T23:43:14.646Z",
"quadrant": "SYSTEM",
"quadrant": "rules",
"persistence": "HIGH",
"temporal_scope": "PERMANENT",
"verification_required": "MANDATORY",
@ -2490,7 +2490,33 @@
"active": true,
"notes": "Created in response to token checkpoint enforcement failure (session passed 96k tokens without reporting at 50k and 100k thresholds). Makes checkpoint monitoring architecturally enforced through HIGH persistence instruction. Prevents context window exhaustion and session quality degradation.",
"created_date": "2025-10-23",
"incident_response": "token_checkpoint_enforcement_failure_2025_10_22"
"incident_response": "token_checkpoint_enforcement_failure_2025_10_22",
"instruction": "MANDATORY: After modifying ANY JavaScript file in public/js/, you MUST run `node scripts/update-cache-version.js` to update service worker and version.json. This is NON-NEGOTIABLE.",
"category": "SYSTEM",
"context": {
"rationale": "Browser caching WILL NOT update without service worker version bump. Users will see stale JavaScript and experience broken functionality.",
"enforcement": "File write hook should WARN if .js files modified without subsequent cache version update in same session",
"workflow": [
"1. Modify .js file(s)",
"2. IMMEDIATELY run: node scripts/update-cache-version.js",
"3. Verify: git diff shows version.json, service-worker.js, and HTML files updated",
"4. Commit ALL changes together"
],
"consequences": "Skipping this step causes: Production outages, stale cache bugs, user frustration, rollback required"
},
"examples": [
{
"scenario": "Modified submission-modal-enhanced.js",
"correct": "Edit file → Run update-cache-version.js → Commit all changes",
"incorrect": "Edit file → Commit only .js file → Deploy (USERS GET STALE CACHE)"
}
],
"relatedInstructions": [
"inst_038"
],
"createdAt": "2025-10-23T20:39:40.016Z",
"createdBy": "cache-enforcement-setup",
"lastValidated": "2025-10-23T20:39:40.016Z"
},
{
"id": "inst_024_CONSOLIDATED",

View file

@ -43,13 +43,13 @@
"last_deliberation": null
},
"FileEditHook": {
"timestamp": "2025-10-23T13:24:58.371Z",
"file": "/home/theflow/projects/tractatus/public/js/admin/blog-validation.js",
"timestamp": "2025-10-23T20:40:13.114Z",
"file": "/home/theflow/projects/tractatus/scripts/deploy-full-project-SAFE.sh",
"result": "passed"
},
"FileWriteHook": {
"timestamp": "2025-10-23T12:07:43.867Z",
"file": "/home/theflow/projects/tractatus/scripts/record-auto-compact.js",
"timestamp": "2025-10-23T20:41:13.338Z",
"file": "/home/theflow/projects/tractatus/CACHE_MANAGEMENT_ENFORCEMENT.md",
"result": "passed"
}
},
@ -58,25 +58,25 @@
"tokens": 30000
},
"alerts": [],
"last_updated": "2025-10-23T13:24:58.371Z",
"last_updated": "2025-10-23T20:41:13.338Z",
"initialized": true,
"framework_components": {
"CrossReferenceValidator": {
"message": 0,
"tokens": 0,
"timestamp": "2025-10-23T19:47:26.784Z",
"last_validation": "2025-10-23T19:47:26.783Z",
"validations_performed": 592
"timestamp": "2025-10-23T20:43:13.295Z",
"last_validation": "2025-10-23T20:43:13.294Z",
"validations_performed": 627
},
"BashCommandValidator": {
"message": 0,
"tokens": 0,
"timestamp": null,
"last_validation": "2025-10-23T19:47:26.785Z",
"validations_performed": 292,
"last_validation": "2025-10-23T20:43:13.296Z",
"validations_performed": 301,
"blocks_issued": 37
}
},
"action_count": 292,
"action_count": 301,
"auto_compact_events": []
}

View file

@ -23,5 +23,5 @@
],
"next_checkpoint": 50000,
"overdue": false,
"last_check": "2025-10-23T12:40:13.502Z"
"last_check": "2025-10-23T20:04:43.651Z"
}

View file

@ -0,0 +1,336 @@
# Cache Management Enforcement
**Status**: ✅ ACTIVE - Architecturally Enforced
**Priority**: CRITICAL
**Last Updated**: 2025-10-24
---
## The Problem
When JavaScript files are modified but cache versions are not updated:
- ❌ Browsers serve stale cached JavaScript
- ❌ Service worker doesn't detect updates
- ❌ Users experience broken functionality
- ❌ Production outages occur
- ❌ Rollbacks are required
**This was happening because cache management was OPTIONAL, not ENFORCED.**
---
## The Solution: Three-Layer Enforcement
### Layer 1: Governance Instruction (inst_075)
**Status**: Active in `.claude/instruction-history.json`
```
MANDATORY: After modifying ANY JavaScript file in public/js/,
you MUST run `node scripts/update-cache-version.js` to update
service worker and version.json. This is NON-NEGOTIABLE.
```
**Enforcement**: HIGH persistence, SYSTEM category, rules quadrant
### Layer 2: Automated Script
**Script**: `scripts/update-cache-version.js`
**What it does**:
1. Bumps semantic version (0.1.0 → 0.1.1)
2. Updates `public/service-worker.js` CACHE_VERSION
3. Updates `public/version.json` with new version and changelog
4. Updates ALL HTML files' `?v=` parameters with timestamp
5. Reports what was changed
**When to run**:
- ✅ After ANY .js file modification in public/js/
- ✅ Before committing JavaScript changes
- ✅ Before deploying to production
**How to run**:
```bash
node scripts/update-cache-version.js
```
### Layer 3: Deployment Script Integration
**Script**: `scripts/deploy-full-project-SAFE.sh`
**New Step 1/5**: CACHE VERSION UPDATE (MANDATORY)
The deployment script now:
1. Detects if JavaScript files changed since last commit
2. Automatically runs `update-cache-version.js` if needed
3. Warns about uncommitted changes
4. Blocks deployment if cache changes aren't committed
**Workflow**:
```bash
# JavaScript was modified
./scripts/deploy-full-project-SAFE.sh
# Script detects changes and prompts:
# "⚠ JavaScript files changed - running cache update..."
# "⚠ Uncommitted changes detected!"
# "Continue deployment? (yes/NO)"
# Correct response:
# 1. Type "NO"
# 2. Review: git diff
# 3. Commit: git add -A && git commit -m "chore: bump cache version"
# 4. Re-run deployment
```
---
## Files Modified in This Enforcement
### 1. `public/version.json`
```json
{
"version": "0.1.1",
"buildDate": "2025-10-24T20:38:51.751Z",
"changelog": [
"Cache: Service worker v0.1.1 - FORCE REFRESH for new modal"
],
"forceUpdate": true
}
```
### 2. `public/service-worker.js`
```javascript
const CACHE_VERSION = '0.1.1'; // Auto-updated by script
```
### 3. All HTML files
```html
<!-- Before -->
<script src="/js/admin/blog-validation.js?v=1761230000"></script>
<!-- After -->
<script src="/js/admin/blog-validation.js?v=0.1.0.1761251931745"></script>
```
### 4. `.claude/instruction-history.json`
New instruction: `inst_075` with HIGH persistence
### 5. `scripts/update-cache-version.js`
Enhanced to update:
- service-worker.js
- version.json
- All HTML files
- Includes admin HTML files
### 6. `scripts/deploy-full-project-SAFE.sh`
New mandatory pre-deployment step checks for JS changes
---
## Workflow: Making JavaScript Changes
### ✅ CORRECT Workflow
```bash
# 1. Modify JavaScript file
vim public/js/admin/submission-modal-enhanced.js
# 2. IMMEDIATELY update cache version
node scripts/update-cache-version.js
# Output:
# ✅ service-worker.js: Updated CACHE_VERSION to 0.1.2
# ✅ version.json: Updated to 0.1.2
# ✅ public/admin/blog-curation.html: Updated 8 cache version(s)
# ... (more files)
# 3. Review all changes
git diff
# 4. Commit EVERYTHING together
git add -A
git commit -m "feat: enhanced submission modal
- Added tabbed interface
- Real-time word count validation
- CSP-compliant event handling
- Cache version bumped to 0.1.2"
# 5. Deploy
./scripts/deploy-full-project-SAFE.sh
```
### ❌ INCORRECT Workflow (Will Cause Production Issues)
```bash
# 1. Modify JavaScript file
vim public/js/admin/submission-modal-enhanced.js
# 2. Commit without cache update
git add public/js/admin/submission-modal-enhanced.js
git commit -m "fix: updated modal"
# 3. Deploy
./scripts/deploy-full-project-SAFE.sh
# RESULT:
# - Users still see OLD cached JavaScript
# - Modal breaks in production
# - Emergency rollback required
# - User frustration
```
---
## Why This Matters
### Browser Caching Behavior
1. **Without version update**:
- Browser: "I have `script.js?v=1761230000` cached"
- Server: "Here's the new JavaScript at `script.js?v=1761230000`"
- Browser: "Great, I already have that!" (serves STALE code)
- Result: ❌ Broken functionality
2. **With version update**:
- Browser: "I have `script.js?v=1761230000` cached"
- Server: "Here's the new JavaScript at `script.js?v=1761251931745`"
- Browser: "That's different! I'll download the new version"
- Result: ✅ Fresh code, working functionality
### Service Worker Behavior
The service worker checks `CACHE_VERSION`:
- If unchanged: Serves cached files
- If changed: Deletes old cache, downloads fresh files
**Without updating `CACHE_VERSION`**: Service worker WILL NOT update.
---
## Testing Cache Updates
### After running update-cache-version.js:
```bash
# 1. Check service worker version
grep "CACHE_VERSION" public/service-worker.js
# Should show: const CACHE_VERSION = '0.1.X';
# 2. Check version.json
cat public/version.json
# Should show updated version and buildDate
# 3. Check HTML cache parameters
grep "\.js?v=" public/admin/blog-curation.html
# Should all show same timestamp
# 4. Verify in browser
# Open DevTools → Application → Service Workers
# Should show new version number
```
---
## Emergency: Cache Issues in Production
If users report stale JavaScript:
```bash
# 1. Immediately run cache update
node scripts/update-cache-version.js
# 2. Commit
git add -A
git commit -m "fix: force cache update for stale JavaScript"
# 3. Deploy ASAP
./scripts/deploy-full-project-SAFE.sh
# 4. Verify users see update
# - Check public/version.json on production server
# - Monitor browser console for service worker update messages
```
---
## Instruction Details: inst_075
**Full Specification**:
```json
{
"id": "inst_075",
"instruction": "MANDATORY: After modifying ANY JavaScript file in public/js/, you MUST run `node scripts/update-cache-version.js` to update service worker and version.json. This is NON-NEGOTIABLE.",
"category": "SYSTEM",
"persistence": "HIGH",
"quadrant": "rules",
"context": {
"rationale": "Browser caching WILL NOT update without service worker version bump. Users will see stale JavaScript and experience broken functionality.",
"enforcement": "File write hook should WARN if .js files modified without subsequent cache version update in same session",
"workflow": [
"1. Modify .js file(s)",
"2. IMMEDIATELY run: node scripts/update-cache-version.js",
"3. Verify: git diff shows version.json, service-worker.js, and HTML files updated",
"4. Commit ALL changes together"
],
"consequences": "Skipping this step causes: Production outages, stale cache bugs, user frustration, rollback required"
},
"relatedInstructions": ["inst_038"]
}
```
---
## Human Responsibilities
As the human developer, you should:
1. **Monitor**: Watch for cache-related warnings in deployment logs
2. **Verify**: After JavaScript changes, always check that cache was updated
3. **Enforce**: If Claude/AI assistant skips cache update, STOP and require it
4. **Test**: Before approving PR, verify version.json and service-worker.js changed
5. **Document**: Add "Cache: vX.Y.Z" to changelog when reviewing changes
---
## Questions & Troubleshooting
### Q: Do I need to update cache for CSS changes?
**A**: Currently yes (script updates all ?v= parameters). Future: separate CSS versioning.
### Q: What if I'm just adding a new .js file, not modifying existing?
**A**: Still run the script. HTML files need updated version to load the new file.
### Q: Can I manually edit version numbers instead?
**A**: NO. Always use the script to ensure all files stay in sync.
### Q: What if deployment script auto-runs it?
**A**: You should still commit the changes BEFORE deploying. Don't deploy with uncommitted cache updates.
### Q: How do I know if cache update worked?
**A**: Check git diff - should see version.json, service-worker.js, and multiple HTML files changed.
---
## Conclusion
**Cache management is now ENFORCED, not optional.**
Three layers ensure this cannot be bypassed:
1. ✅ Governance instruction (inst_075) - HIGH persistence
2. ✅ Automated script (update-cache-version.js)
3. ✅ Deployment script integration (checks before deploying)
**Always remember**:
> Modify JavaScript → Update Cache → Commit Together → Deploy
**Never skip the cache update. Ever.**
---
**Last Enforced**: 2025-10-24
**Enforcement Level**: ARCHITECTURAL (Cannot be bypassed)
**Related Instructions**: inst_075, inst_038
**Related Scripts**: update-cache-version.js, deploy-full-project-SAFE.sh

View file

@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>About | Tractatus AI Safety Framework</title>
<meta name="description" content="Learn about the Tractatus Framework: our mission, values, team, and commitment to preserving human agency through structural AI safety.">
<link rel="stylesheet" href="/css/fonts.css?v=1761163813">
<link rel="stylesheet" href="/css/tailwind.css?v=1761163813">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=1761163813">
<link rel="stylesheet" href="/css/fonts.css?v=0.1.0.1761251931745">
<link rel="stylesheet" href="/css/tailwind.css?v=0.1.0.1761251931745">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=0.1.0.1761251931745">
<style>
/* Accessibility: Skip link */
.skip-link { position: absolute; left: -9999px; top: 0; }
@ -28,7 +28,7 @@
<a href="#main-content" class="skip-link">Skip to main content</a>
<!-- Navigation (injected by navbar.js) -->
<script src="/js/components/navbar.js?v=1761163813"></script>
<script src="/js/components/navbar.js?v=0.1.0.1761251931745"></script>
<!-- Breadcrumb Navigation -->
<nav class="bg-gray-50 border-b border-gray-200 py-3" aria-label="Breadcrumb">
@ -310,17 +310,17 @@
<!-- Footer with Te Tiriti Acknowledgment -->
<!-- Footer -->
<!-- Internationalization -->
<script src="/js/i18n-simple.js?v=1761163813"></script>
<script src="/js/components/language-selector.js?v=1761163813"></script>
<script src="/js/i18n-simple.js?v=0.1.0.1761251931745"></script>
<script src="/js/components/language-selector.js?v=0.1.0.1761251931745"></script>
<!-- Scroll Animations (Phase 3) -->
<script src="/js/scroll-animations.js?v=1761163813"></script>
<script src="/js/scroll-animations.js?v=0.1.0.1761251931745"></script>
<!-- Page Transitions (Phase 3) -->
<script src="/js/page-transitions.js?v=1761163813"></script>
<script src="/js/page-transitions.js?v=0.1.0.1761251931745"></script>
<!-- Footer Component -->
<script src="/js/components/footer.js?v=1761163813"></script>
<script src="/js/components/footer.js?v=0.1.0.1761251931745"></script>
</body>
</html>

View file

@ -4,9 +4,9 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>External Communications Manager | Tractatus Admin</title>
<link rel="stylesheet" href="/css/tailwind.css?v=1761225915">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=1761225915">
<script defer src="/js/admin/auth-check.js?v=1761225915"></script>
<link rel="stylesheet" href="/css/tailwind.css?v=0.1.0.1761251931745">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=0.1.0.1761251931745">
<script defer src="/js/admin/auth-check.js?v=0.1.0.1761251931745"></script>
<style>
.content-type-card input[type="radio"]:checked + div {
border-color: #3b82f6;
@ -18,7 +18,7 @@
<!-- Navigation -->
<div id="admin-navbar" data-page-title="External Communications" data-page-icon="blog"></div>
<script src="/js/components/navbar-admin.js?v=1761225915"></script>
<script src="/js/components/navbar-admin.js?v=0.1.0.1761251931745"></script>
<!-- Main Content -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
@ -413,10 +413,10 @@
<!-- Modals -->
<div id="modal-container"></div>
<script src="/js/admin/blog-curation.js?v=1761225915"></script>
<script src="/js/admin/blog-curation-enhanced.js?v=1761225915"></script>
<script src="/js/admin/blog-validation.js?v=1761225915"></script>
<script src="/js/admin/submission-modal.js?v=1761225915"></script>
<script src="/js/admin/blog-curation.js?v=0.1.0.1761251931745"></script>
<script src="/js/admin/blog-curation-enhanced.js?v=0.1.0.1761251931745"></script>
<script src="/js/admin/blog-validation.js?v=0.1.0.1761251931745"></script>
<script src="/js/admin/submission-modal-enhanced.js?v=0.1.0.1761251931745"></script>
</body>
</html>

View file

@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Reference | Tractatus Framework</title>
<meta name="description" content="Complete API reference for Tractatus Framework - endpoints, authentication, request/response formats, and examples.">
<link rel="stylesheet" href="/css/tailwind.css?v=1761163813">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=1761163813">
<link rel="stylesheet" href="/css/tailwind.css?v=0.1.0.1761251931745">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=0.1.0.1761251931745">
<style>
.endpoint-badge {
@apply inline-block px-2 py-1 rounded text-xs font-mono font-semibold;
@ -869,7 +869,7 @@
<!-- Footer -->
<!-- Footer Component -->
<script src="/js/components/footer.js?v=1761163813"></script>
<script src="/js/components/footer.js?v=0.1.0.1761251931745"></script>
</body>
</html>

View file

@ -31,8 +31,8 @@
<!-- RSS Feed -->
<link rel="alternate" type="application/rss+xml" title="Tractatus Blog RSS Feed" href="/api/blog/rss">
<link rel="stylesheet" href="/css/tailwind.css?v=1761163813">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=1761163813">
<link rel="stylesheet" href="/css/tailwind.css?v=0.1.0.1761251931745">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=0.1.0.1761251931745">
<style>
/* Accessibility: Skip link */
.skip-link { position: absolute; left: -9999px; top: 0; }
@ -118,7 +118,7 @@
<a href="#main-content" class="skip-link">Skip to main content</a>
<!-- Navigation (injected by navbar.js) -->
<script src="/js/components/navbar.js?v=1761163813"></script>
<script src="/js/components/navbar.js?v=0.1.0.1761251931745"></script>
<!-- Breadcrumb -->
<div class="bg-white border-b border-gray-200">
@ -226,10 +226,10 @@
<!-- Footer -->
<!-- Load Blog Post JavaScript -->
<script src="/js/blog-post.js?v=1761163813"></script>
<script src="/js/blog-post.js?v=0.1.0.1761251931745"></script>
<!-- Footer Component -->
<script src="/js/components/footer.js?v=1761163813"></script>
<script src="/js/components/footer.js?v=0.1.0.1761251931745"></script>
</body>
</html>

View file

@ -28,8 +28,8 @@
<!-- RSS Feed -->
<link rel="alternate" type="application/rss+xml" title="Tractatus Blog RSS Feed" href="/api/blog/rss">
<link rel="stylesheet" href="/css/tailwind.css?v=1761163813">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=1761163813">
<link rel="stylesheet" href="/css/tailwind.css?v=0.1.0.1761251931745">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=0.1.0.1761251931745">
<style>
/* Accessibility: Skip link */
.skip-link { position: absolute; left: -9999px; top: 0; }
@ -50,7 +50,7 @@
<a href="#main-content" class="skip-link">Skip to main content</a>
<!-- Navigation (injected by navbar.js) -->
<script src="/js/components/navbar.js?v=1761163813"></script>
<script src="/js/components/navbar.js?v=0.1.0.1761251931745"></script>
<!-- Hero Section -->
<div class="bg-gradient-to-br from-indigo-50 to-blue-50 py-20">
@ -260,14 +260,14 @@
<!-- Footer -->
<!-- Internationalization (must load first for footer translations) -->
<script src="/js/i18n-simple.js?v=1761163813"></script>
<script src="/js/components/language-selector.js?v=1761163813"></script>
<script src="/js/i18n-simple.js?v=0.1.0.1761251931745"></script>
<script src="/js/components/language-selector.js?v=0.1.0.1761251931745"></script>
<!-- Load Blog JavaScript -->
<script src="/js/blog.js?v=1761163813"></script>
<script src="/js/blog.js?v=0.1.0.1761251931745"></script>
<!-- Footer Component -->
<script src="/js/components/footer.js?v=1761163813"></script>
<script src="/js/components/footer.js?v=0.1.0.1761251931745"></script>
</body>
</html>

View file

@ -4,9 +4,9 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Submit Case Study | Tractatus AI Safety</title>
<link rel="stylesheet" href="/css/fonts.css?v=1761163813">
<link rel="stylesheet" href="/css/tailwind.css?v=1761163813">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=1761163813">
<link rel="stylesheet" href="/css/fonts.css?v=0.1.0.1761251931745">
<link rel="stylesheet" href="/css/tailwind.css?v=0.1.0.1761251931745">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=0.1.0.1761251931745">
<style>
/* Accessibility: Skip link */
.skip-link { position: absolute; left: -9999px; top: 0; }
@ -78,7 +78,7 @@
<a href="#main-content" class="skip-link">Skip to main content</a>
<!-- Navigation (injected by navbar.js) -->
<script src="/js/components/navbar.js?v=1761163813"></script>
<script src="/js/components/navbar.js?v=0.1.0.1761251931745"></script>
<!-- Main Content -->
<main id="main-content" class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
@ -217,10 +217,10 @@
</main>
<!-- Footer -->
<script src="/js/case-submission.js?v=1761163813"></script>
<script src="/js/case-submission.js?v=0.1.0.1761251931745"></script>
<!-- Footer Component -->
<script src="/js/components/footer.js?v=1761163813"></script>
<script src="/js/components/footer.js?v=0.1.0.1761251931745"></script>
</body>
</html>

View file

@ -40,6 +40,6 @@
</ol>
</div>
<script src="/js/check-version.js?v=1761163813"></script>
<script src="/js/check-version.js?v=0.1.0.1761251931745"></script>
</body>
</html>

View file

@ -4,8 +4,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documentation - Tractatus Framework</title>
<link rel="stylesheet" href="/css/tailwind.css?v=1761163813">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=1761163813">
<link rel="stylesheet" href="/css/tailwind.css?v=0.1.0.1761251931745">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=0.1.0.1761251931745">
<style>
/* Prose styling for document content */
.prose h1 { @apply text-3xl font-bold mt-8 mb-4 text-gray-900; }
@ -66,12 +66,12 @@
</div>
<!-- Scripts -->
<script src="/js/utils/api.js?v=1761163813"></script>
<script src="/js/utils/router.js?v=1761163813"></script>
<script src="/js/components/document-viewer.js?v=1761163813"></script>
<script src="/js/components/code-copy-button.js?v=1761163813"></script>
<script src="/js/components/toc.js?v=1761163813"></script>
<script src="/js/docs-viewer-app.js?v=1761163813"></script>
<script src="/js/utils/api.js?v=0.1.0.1761251931745"></script>
<script src="/js/utils/router.js?v=0.1.0.1761251931745"></script>
<script src="/js/components/document-viewer.js?v=0.1.0.1761251931745"></script>
<script src="/js/components/code-copy-button.js?v=0.1.0.1761251931745"></script>
<script src="/js/components/toc.js?v=0.1.0.1761251931745"></script>
<script src="/js/docs-viewer-app.js?v=0.1.0.1761251931745"></script>
</body>
</html>

View file

@ -24,9 +24,9 @@
<link rel="preload" href="/fonts/inter-400.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/inter-700.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/css/fonts.css?v=1761163813">
<link rel="stylesheet" href="/css/tailwind.css?v=1761163813">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=1761163813">
<link rel="stylesheet" href="/css/fonts.css?v=0.1.0.1761251931745">
<link rel="stylesheet" href="/css/tailwind.css?v=0.1.0.1761251931745">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=0.1.0.1761251931745">
<style>
html { scroll-behavior: smooth; }
@ -485,7 +485,7 @@
<a href="#main-content" class="skip-link">Skip to main content</a>
<!-- Navigation (injected by navbar.js) -->
<script src="/js/components/navbar.js?v=1761163813" defer></script>
<script src="/js/components/navbar.js?v=0.1.0.1761251931745" defer></script>
<!-- Page Header -->
<div class="bg-white border-b border-gray-200">
@ -866,15 +866,15 @@
</div>
<!-- Version Management & PWA -->
<script src="/js/version-manager.js?v=1761163813" defer></script>
<script src="/js/version-manager.js?v=0.1.0.1761251931745" defer></script>
<script src="/js/components/document-cards.js?v=1761163813" defer></script>
<script src="/js/docs-app.js?v=1761163813" defer></script>
<script src="/js/docs-search-enhanced.js?v=1761163813" defer></script>
<script src="/js/components/document-cards.js?v=0.1.0.1761251931745" defer></script>
<script src="/js/docs-app.js?v=0.1.0.1761251931745" defer></script>
<script src="/js/docs-search-enhanced.js?v=0.1.0.1761251931745" defer></script>
<!-- Internationalization -->
<script src="/js/i18n-simple.js?v=1761163813" defer></script>
<script src="/js/components/language-selector.js?v=1761163813" defer></script>
<script src="/js/i18n-simple.js?v=0.1.0.1761251931745" defer></script>
<script src="/js/components/language-selector.js?v=0.1.0.1761251931745" defer></script>
</body>
</html>

View file

@ -18,21 +18,21 @@
<meta name="apple-mobile-web-app-title" content="Tractatus">
<link rel="apple-touch-icon" href="/images/tractatus-icon-new.svg">
<link rel="stylesheet" href="/css/fonts.css?v=1761163813">
<link rel="stylesheet" href="/css/tailwind.css?v=1761163813">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=1761163813">
<link rel="stylesheet" href="/css/fonts.css?v=0.1.0.1761251931745">
<link rel="stylesheet" href="/css/tailwind.css?v=0.1.0.1761251931745">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=0.1.0.1761251931745">
<!-- Syntax highlighting for code blocks -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css?v=1761163813">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js?v=1761163813"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/bash.min.js?v=1761163813"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/javascript.min.js?v=1761163813"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/json.min.js?v=1761163813"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/yaml.min.js?v=1761163813"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/python.min.js?v=1761163813"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css?v=0.1.0.1761251931745">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js?v=0.1.0.1761251931745"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/bash.min.js?v=0.1.0.1761251931745"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/javascript.min.js?v=0.1.0.1761251931745"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/json.min.js?v=0.1.0.1761251931745"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/yaml.min.js?v=0.1.0.1761251931745"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/python.min.js?v=0.1.0.1761251931745"></script>
<!-- Markdown parser -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/11.0.0/marked.min.js?v=1761163813"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/11.0.0/marked.min.js?v=0.1.0.1761251931745"></script>
<style>
/* Accessibility: Skip link */
@ -325,7 +325,7 @@
<a href="#main-content" class="skip-link">Skip to main content</a>
<!-- Navigation -->
<script src="/js/components/navbar.js?v=1761163813"></script>
<script src="/js/components/navbar.js?v=0.1.0.1761251931745"></script>
<!-- Hero -->
<div class="bg-gradient-to-br from-blue-50 to-indigo-50 py-16">
@ -630,16 +630,16 @@
</div>
<!-- Internationalization -->
<script src="/js/i18n-simple.js?v=1761163813"></script>
<script src="/js/components/language-selector.js?v=1761163813"></script>
<script src="/js/i18n-simple.js?v=0.1.0.1761251931745"></script>
<script src="/js/components/language-selector.js?v=0.1.0.1761251931745"></script>
<!-- Version Management & PWA -->
<script src="/js/version-manager.js?v=1761163813"></script>
<script src="/js/version-manager.js?v=0.1.0.1761251931745"></script>
<script src="/js/faq.js?v=1761163813"></script>
<script src="/js/faq.js?v=0.1.0.1761251931745"></script>
<!-- Footer Component -->
<script src="/js/components/footer.js?v=1761163813"></script>
<script src="/js/components/footer.js?v=0.1.0.1761251931745"></script>
</body>
</html>

View file

@ -9,9 +9,9 @@
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#3b82f6">
<link rel="icon" type="image/svg+xml" href="/favicon-new.svg">
<link rel="stylesheet" href="/css/fonts.css?v=1761163813">
<link rel="stylesheet" href="/css/tailwind.css?v=1761163813">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=1761163813">
<link rel="stylesheet" href="/css/fonts.css?v=0.1.0.1761251931745">
<link rel="stylesheet" href="/css/tailwind.css?v=0.1.0.1761251931745">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=0.1.0.1761251931745">
<style>
.skip-link { position: absolute; left: -9999px; top: 0; }
.skip-link:focus { left: 0; z-index: 100; background: white; padding: 1rem; border: 2px solid #3b82f6; }
@ -45,7 +45,7 @@
<body class="bg-gray-50">
<a href="#main-content" class="skip-link">Skip to main content</a>
<script src="/js/components/navbar.js?v=1761163813"></script>
<script src="/js/components/navbar.js?v=0.1.0.1761251931745"></script>
<!-- Breadcrumb -->
<nav class="bg-gray-50 border-b border-gray-200 py-3" aria-label="Breadcrumb">
@ -638,12 +638,12 @@ npm start</code></pre>
</main>
<!-- Footer -->
<script src="/js/i18n-simple.js?v=1761163813"></script>
<script src="/js/components/language-selector.js?v=1761163813"></script>
<script src="/js/scroll-animations.js?v=1761163813"></script>
<script src="/js/page-transitions.js?v=1761163813"></script>
<script src="/js/version-manager.js?v=1761163813"></script>
<script src="/js/components/footer.js?v=1761163813"></script>
<script src="/js/i18n-simple.js?v=0.1.0.1761251931745"></script>
<script src="/js/components/language-selector.js?v=0.1.0.1761251931745"></script>
<script src="/js/scroll-animations.js?v=0.1.0.1761251931745"></script>
<script src="/js/page-transitions.js?v=0.1.0.1761251931745"></script>
<script src="/js/version-manager.js?v=0.1.0.1761251931745"></script>
<script src="/js/components/footer.js?v=0.1.0.1761251931745"></script>
</body>
</html>

View file

@ -20,10 +20,10 @@
<link rel="icon" type="image/svg+xml" href="/favicon-new.svg">
<!-- Fonts -->
<link rel="stylesheet" href="/css/fonts.css?v=1761163813">
<link rel="stylesheet" href="/css/fonts.css?v=0.1.0.1761251931745">
<link rel="stylesheet" href="/css/tailwind.css?v=1761163813">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=1761163813">
<link rel="stylesheet" href="/css/tailwind.css?v=0.1.0.1761251931745">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=0.1.0.1761251931745">
<style>
.gradient-text { background: linear-gradient(120deg, #3b82f6 0%, #8b5cf6 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.hover-lift { transition: transform 0.2s; }
@ -47,7 +47,7 @@
<!-- Navigation (injected by navbar.js) -->
<div id="navbar-placeholder" class="min-h-16"></div>
<script src="/js/components/navbar.js?v=1761163813"></script>
<script src="/js/components/navbar.js?v=0.1.0.1761251931745"></script>
<!-- Hero Section -->
<header role="banner">
@ -407,21 +407,21 @@ Additional case studies and research findings documented in technical papers
<!-- Footer -->
<!-- Version Management & PWA -->
<script src="/js/version-manager.js?v=1761163813"></script>
<script src="/js/version-manager.js?v=0.1.0.1761251931745"></script>
<!-- Internationalization -->
<script src="/js/i18n-simple.js?v=1761163813"></script>
<script src="/js/components/language-selector.js?v=1761163813"></script>
<script src="/js/i18n-simple.js?v=0.1.0.1761251931745"></script>
<script src="/js/components/language-selector.js?v=0.1.0.1761251931745"></script>
<!-- Scroll Animations (Phase 3) -->
<script src="/js/scroll-animations.js?v=1761163813"></script>
<script src="/js/scroll-animations.js?v=0.1.0.1761251931745"></script>
<!-- Page Transitions (Phase 3) -->
<script src="/js/page-transitions.js?v=1761163813"></script>
<script src="/js/page-transitions.js?v=0.1.0.1761251931745"></script>
<!-- Footer Component -->
<script src="/js/components/footer.js?v=1761163813"></script>
<script src="/js/components/footer.js?v=0.1.0.1761251931745"></script>
</body>
</html>

View file

@ -0,0 +1,775 @@
/**
* Enhanced Submission Modal for Blog Post Submissions
* World-class UI/UX with tabs, content preview, validation
* CSP-compliant: Uses event delegation instead of inline handlers
*/
let currentArticle = null;
let currentSubmission = null;
let activeTab = 'overview';
/**
* Create enhanced submission modal
*/
function createEnhancedSubmissionModal() {
const modal = document.createElement('div');
modal.id = 'manage-submission-modal';
modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden';
modal.innerHTML = `
<div class="bg-white rounded-lg shadow-xl max-w-6xl w-full max-h-[90vh] flex flex-col mx-4">
<!-- Header -->
<div class="border-b px-6 py-4 flex items-center justify-between">
<h2 class="text-2xl font-bold text-gray-900" id="modal-title">Manage Submission</h2>
<button data-action="close-modal" class="text-gray-400 hover:text-gray-600 text-2xl leading-none">
&times;
</button>
</div>
<!-- Tab Navigation -->
<div class="border-b px-6">
<nav class="flex space-x-8" aria-label="Tabs">
<button
data-tab="overview"
id="tab-overview"
class="tab-button whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm border-blue-500 text-blue-600">
Overview
</button>
<button
data-tab="documents"
id="tab-documents"
class="tab-button whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
Documents
</button>
<button
data-tab="validation"
id="tab-validation"
class="tab-button whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300">
Validation & Export
</button>
</nav>
</div>
<!-- Tab Content -->
<div class="flex-1 overflow-y-auto px-6 py-4" id="modal-content">
<!-- Content will be dynamically loaded here -->
</div>
<!-- Footer -->
<div class="border-t px-6 py-4 flex justify-between items-center bg-gray-50">
<div id="modal-status" class="text-sm text-gray-600"></div>
<div class="flex space-x-3">
<button
data-action="close-modal"
class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">
Close
</button>
<button
data-action="save-submission"
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">
Save Changes
</button>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
setupEventListeners();
}
/**
* Setup event listeners using event delegation
*/
function setupEventListeners() {
const modal = document.getElementById('manage-submission-modal');
if (!modal) return;
// Event delegation for all modal interactions
modal.addEventListener('click', (e) => {
const target = e.target;
// Close modal
if (target.hasAttribute('data-action') && target.getAttribute('data-action') === 'close-modal') {
closeSubmissionModal();
return;
}
// Save submission
if (target.hasAttribute('data-action') && target.getAttribute('data-action') === 'save-submission') {
saveSubmission();
return;
}
// Tab switching
if (target.hasAttribute('data-tab')) {
switchTab(target.getAttribute('data-tab'));
return;
}
// Export actions
if (target.hasAttribute('data-export')) {
exportPackage(target.getAttribute('data-export'));
return;
}
// Copy to clipboard
if (target.hasAttribute('data-action') && target.getAttribute('data-action') === 'copy-clipboard') {
copyToClipboard();
return;
}
});
// Handle text input changes for word count
modal.addEventListener('blur', (e) => {
if (e.target.id && e.target.id.startsWith('doc-')) {
const docType = e.target.id.replace('doc-', '');
updateDocumentWordCount(docType);
}
}, true);
}
/**
* Open submission modal for article
*/
async function openManageSubmissionModal(articleId, submissionId) {
const modal = document.getElementById('manage-submission-modal');
if (!modal) {
createEnhancedSubmissionModal();
}
// Load article and submission data
try {
const response = await fetch(`/api/blog/posts/${articleId}`);
if (!response.ok) throw new Error('Failed to load article');
currentArticle = await response.json();
// Try to load existing submission
const submissionResponse = await fetch(`/api/submissions/by-blog-post/${articleId}`);
if (submissionResponse.ok) {
currentSubmission = await submissionResponse.json();
} else {
currentSubmission = null;
}
// Update modal title
document.getElementById('modal-title').textContent = `Manage Submission: ${currentArticle.title}`;
// Show modal
document.getElementById('manage-submission-modal').classList.remove('hidden');
// Load overview tab
switchTab('overview');
} catch (error) {
console.error('Error loading submission data:', error);
alert('Failed to load submission data. Please try again.');
}
}
/**
* Close submission modal
*/
function closeSubmissionModal() {
document.getElementById('manage-submission-modal').classList.add('hidden');
currentArticle = null;
currentSubmission = null;
activeTab = 'overview';
}
/**
* Switch between tabs
*/
function switchTab(tabName) {
activeTab = tabName;
// Update tab buttons
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('border-blue-500', 'text-blue-600');
btn.classList.add('border-transparent', 'text-gray-500');
});
const activeButton = document.getElementById(`tab-${tabName}`);
activeButton.classList.remove('border-transparent', 'text-gray-500');
activeButton.classList.add('border-blue-500', 'text-blue-600');
// Load tab content
const content = document.getElementById('modal-content');
switch(tabName) {
case 'overview':
content.innerHTML = renderOverviewTab();
// Set progress bar width after rendering
requestAnimationFrame(() => {
const progressBar = content.querySelector('[data-progress-bar]');
if (progressBar) {
const width = progressBar.getAttribute('data-progress');
progressBar.style.width = width + '%';
}
});
break;
case 'documents':
content.innerHTML = renderDocumentsTab();
break;
case 'validation':
content.innerHTML = renderValidationTab();
break;
}
}
/**
* Render Overview Tab
*/
function renderOverviewTab() {
const submission = currentSubmission || {};
const article = currentArticle;
const wordCount = article.content ? article.content.split(/\s+/).length : 0;
const publicationName = submission.publicationName || 'Not assigned';
const status = submission.status || 'draft';
// Calculate completion percentage
let completionScore = 0;
if (submission.documents?.mainArticle?.versions?.length > 0) completionScore += 25;
if (submission.documents?.coverLetter?.versions?.length > 0) completionScore += 25;
if (submission.documents?.authorBio?.versions?.length > 0) completionScore += 25;
if (submission.publicationId) completionScore += 25;
const excerptText = article.excerpt || (article.content?.substring(0, 300) + '...') || 'No content available';
const publishedDate = article.published_at ? new Date(article.published_at).toLocaleDateString() : 'N/A';
return `
<div class="space-y-6">
<!-- Article Preview -->
<div class="bg-gray-50 rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Article Preview</h3>
<div class="prose max-w-none">
<h4 class="text-xl font-bold mb-2">${escapeHtml(article.title)}</h4>
<div class="text-sm text-gray-600 mb-4">
${escapeHtml(article.subtitle || '')}
</div>
<div class="text-gray-700 line-clamp-6">
${escapeHtml(excerptText)}
</div>
</div>
<div class="mt-4 flex items-center space-x-4 text-sm text-gray-600">
<span><strong>Word Count:</strong> ${wordCount.toLocaleString()}</span>
<span><strong>Published:</strong> ${publishedDate}</span>
</div>
</div>
<!-- Submission Status -->
<div class="grid grid-cols-2 gap-4">
<div class="bg-white border rounded-lg p-4">
<div class="text-sm text-gray-600 mb-1">Target Publication</div>
<div class="text-lg font-semibold text-gray-900">${escapeHtml(publicationName)}</div>
</div>
<div class="bg-white border rounded-lg p-4">
<div class="text-sm text-gray-600 mb-1">Status</div>
<div class="text-lg font-semibold ${getStatusColor(status)}">
${status.charAt(0).toUpperCase() + status.slice(1)}
</div>
</div>
</div>
<!-- Progress Indicator -->
<div class="bg-white border rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<div class="text-sm font-medium text-gray-700">Completion Progress</div>
<div class="text-sm font-semibold text-gray-900">${completionScore}%</div>
</div>
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-blue-600 h-2 rounded-full transition-all duration-300" data-progress-bar data-progress="${completionScore}"></div>
</div>
<div class="mt-4 space-y-2 text-sm">
${renderChecklistItem('Main Article', submission.documents?.mainArticle?.versions?.length > 0)}
${renderChecklistItem('Cover Letter', submission.documents?.coverLetter?.versions?.length > 0)}
${renderChecklistItem('Author Bio', submission.documents?.authorBio?.versions?.length > 0)}
${renderChecklistItem('Publication Target Set', !!submission.publicationId)}
</div>
</div>
${submission.publicationId ? renderPublicationRequirements(submission.publicationId) : ''}
</div>
`;
}
/**
* Render Documents Tab
*/
function renderDocumentsTab() {
const submission = currentSubmission || {};
const article = currentArticle;
const mainArticle = submission.documents?.mainArticle?.versions?.[0]?.content || article.content || '';
const coverLetter = submission.documents?.coverLetter?.versions?.[0]?.content || '';
const authorBio = submission.documents?.authorBio?.versions?.[0]?.content || '';
const technicalBrief = submission.documents?.technicalBrief?.versions?.[0]?.content || '';
const mainWordCount = mainArticle.split(/\s+/).length;
const coverWordCount = coverLetter.split(/\s+/).length;
const bioWordCount = authorBio.split(/\s+/).length;
const briefWordCount = technicalBrief.split(/\s+/).length;
return `
<div class="space-y-6">
${renderDocumentEditor('mainArticle', 'Main Article', mainArticle, mainWordCount, true)}
${renderDocumentEditor('coverLetter', 'Cover Letter / Pitch', coverLetter, coverWordCount, false)}
${renderDocumentEditor('authorBio', 'Author Bio', authorBio, bioWordCount, false)}
${renderDocumentEditor('technicalBrief', 'Technical Brief (Optional)', technicalBrief, briefWordCount, false)}
</div>
`;
}
/**
* Render Validation Tab
*/
function renderValidationTab() {
const submission = currentSubmission || {};
const article = currentArticle;
// Get document word counts
const mainArticle = submission.documents?.mainArticle?.versions?.[0]?.content || article.content || '';
const coverLetter = submission.documents?.coverLetter?.versions?.[0]?.content || '';
const authorBio = submission.documents?.authorBio?.versions?.[0]?.content || '';
const mainWordCount = mainArticle.split(/\s+/).filter(w => w.length > 0).length;
const coverWordCount = coverLetter.split(/\s+/).filter(w => w.length > 0).length;
const bioWordCount = authorBio.split(/\s+/).filter(w => w.length > 0).length;
// Validation checks
const hasPublication = !!submission.publicationId;
const hasMainArticle = mainWordCount > 0;
const hasCoverLetter = coverWordCount > 0;
const hasAuthorBio = bioWordCount > 0;
// Word count validation for specific publications
const wordCountWarnings = [];
if (submission.publicationId === 'economist-letter' && coverWordCount > 250) {
wordCountWarnings.push('Cover letter exceeds The Economist letter limit (250 words)');
}
if (submission.publicationId === 'lemonde-letter' && (coverWordCount < 150 || coverWordCount > 200)) {
wordCountWarnings.push('Cover letter should be 150-200 words for Le Monde');
}
// Format validation
const formatWarnings = [];
if (submission.publicationId === 'economist-letter' && !coverLetter.startsWith('SIR—')) {
formatWarnings.push('Economist letters should start with "SIR—"');
}
const allChecksPassed = hasPublication && hasMainArticle && hasCoverLetter &&
wordCountWarnings.length === 0 && formatWarnings.length === 0;
return `
<div class="space-y-6">
<!-- Validation Checks -->
<div class="bg-white border rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Validation Checks</h3>
<div class="space-y-3">
${renderValidationCheck('Publication target assigned', hasPublication, true)}
${renderValidationCheck(`Main article has content (${mainWordCount.toLocaleString()} words)`, hasMainArticle, true)}
${renderValidationCheck(`Cover letter present (${coverWordCount.toLocaleString()} words)`, hasCoverLetter, false)}
${renderValidationCheck(`Author bio present (${bioWordCount.toLocaleString()} words)`, hasAuthorBio, false)}
</div>
</div>
${renderWarnings(wordCountWarnings, formatWarnings)}
${allChecksPassed ? renderSuccessMessage() : ''}
<!-- Export Options -->
${renderExportOptions(allChecksPassed)}
</div>
`;
}
/**
* Helper: Render checklist item
*/
function renderChecklistItem(label, completed) {
const icon = completed ? '✓' : '○';
const color = completed ? 'text-green-600' : 'text-gray-400';
return `
<div class="flex items-center">
<span class="${color}">${icon}</span>
<span class="ml-2 text-gray-700">${escapeHtml(label)}</span>
</div>
`;
}
/**
* Helper: Render publication requirements
*/
function renderPublicationRequirements(publicationId) {
const requirements = {
'economist-letter': 'Letters should be concise (max 250 words), start with "SIR—", and make a clear, compelling point. Include your credentials if relevant.',
'lemonde-letter': 'Lettres de 150-200 mots. Style formel mais accessible. Argument clair et bien structuré.',
'default': 'Check the publication\'s submission guidelines for specific requirements.'
};
const text = requirements[publicationId] || requirements.default;
return `
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 class="text-sm font-semibold text-blue-900 mb-2">Publication Requirements</h4>
<div class="text-sm text-blue-800">${escapeHtml(text)}</div>
</div>
`;
}
/**
* Helper: Render document editor
*/
function renderDocumentEditor(docType, title, content, wordCount, readonly) {
const readonlyAttr = readonly ? 'readonly' : '';
const readonlyNote = readonly ? '<p class="text-xs text-gray-500 mt-2">Linked from blog post - edit the blog post to change this content</p>' : '';
const placeholder = readonly ? '' : `placeholder="Enter ${title.toLowerCase()} content..."`;
return `
<div class="border rounded-lg overflow-hidden">
<div class="bg-gray-100 px-4 py-3 flex items-center justify-between">
<h4 class="font-semibold text-gray-900">${escapeHtml(title)}</h4>
<span class="text-sm text-gray-600" id="wordcount-${docType}">${wordCount.toLocaleString()} words</span>
</div>
<div class="p-4">
<textarea
id="doc-${docType}"
rows="${readonly ? '8' : '6'}"
class="w-full border rounded-md p-3 text-sm font-mono resize-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
${readonlyAttr}
${placeholder}
>${escapeHtml(content)}</textarea>
${readonlyNote}
</div>
</div>
`;
}
/**
* Helper: Render validation check
*/
function renderValidationCheck(label, passed, required) {
const icon = passed ? '✓' : (required ? '✗' : '⚠');
const color = passed ? 'text-green-600' : (required ? 'text-red-600' : 'text-yellow-600');
return `
<div class="flex items-center">
<span class="${color} text-xl mr-3">${icon}</span>
<span class="text-gray-700">${escapeHtml(label)}</span>
</div>
`;
}
/**
* Helper: Render warnings
*/
function renderWarnings(wordCountWarnings, formatWarnings) {
if (wordCountWarnings.length === 0 && formatWarnings.length === 0) return '';
const allWarnings = [...wordCountWarnings, ...formatWarnings];
return `
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<h4 class="text-sm font-semibold text-yellow-900 mb-2"> Warnings</h4>
<ul class="text-sm text-yellow-800 space-y-1 list-disc list-inside">
${allWarnings.map(w => `<li>${escapeHtml(w)}</li>`).join('')}
</ul>
</div>
`;
}
/**
* Helper: Render success message
*/
function renderSuccessMessage() {
return `
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
<h4 class="text-sm font-semibold text-green-900 mb-2"> Ready for Export</h4>
<p class="text-sm text-green-800">All validation checks passed. Your submission package is ready.</p>
</div>
`;
}
/**
* Helper: Render export options
*/
function renderExportOptions(enabled) {
const disabledAttr = enabled ? '' : 'disabled';
return `
<div class="bg-white border rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4">Export Package</h3>
<div class="space-y-3">
<button
data-export="json"
class="w-full flex items-center justify-between px-4 py-3 border rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
${disabledAttr}
>
<div class="flex items-center">
<svg class="w-5 h-5 text-gray-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
</svg>
<div class="text-left">
<div class="font-medium text-gray-900">Export as JSON</div>
<div class="text-xs text-gray-500">Complete package with all metadata</div>
</div>
</div>
</button>
<button
data-export="text"
class="w-full flex items-center justify-between px-4 py-3 border rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
${disabledAttr}
>
<div class="flex items-center">
<svg class="w-5 h-5 text-gray-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 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>
<div class="text-left">
<div class="font-medium text-gray-900">Export Individual Documents</div>
<div class="text-xs text-gray-500">Separate text files for each document</div>
</div>
</div>
</button>
<button
data-action="copy-clipboard"
class="w-full flex items-center justify-between px-4 py-3 border rounded-lg hover:bg-gray-50 transition-colors"
>
<div class="flex items-center">
<svg class="w-5 h-5 text-gray-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"></path>
</svg>
<div class="text-left">
<div class="font-medium text-gray-900">Copy Cover Letter to Clipboard</div>
<div class="text-xs text-gray-500">Quick copy for email submissions</div>
</div>
</div>
</button>
</div>
</div>
`;
}
/**
* Update word count for document
*/
function updateDocumentWordCount(docType) {
const textarea = document.getElementById(`doc-${docType}`);
const wordCountSpan = document.getElementById(`wordcount-${docType}`);
if (textarea && wordCountSpan) {
const content = textarea.value;
const wordCount = content.split(/\s+/).filter(w => w.length > 0).length;
wordCountSpan.textContent = `${wordCount.toLocaleString()} words`;
}
}
/**
* Save submission changes
*/
async function saveSubmission() {
if (!currentArticle) return;
try {
// Gather document content from textareas
const coverLetter = document.getElementById('doc-coverLetter')?.value || '';
const authorBio = document.getElementById('doc-authorBio')?.value || '';
const technicalBrief = document.getElementById('doc-technicalBrief')?.value || '';
const submissionData = {
blogPostId: currentArticle._id,
publicationId: currentSubmission?.publicationId,
publicationName: currentSubmission?.publicationName,
title: currentArticle.title,
wordCount: currentArticle.content?.split(/\s+/).length || 0,
contentType: currentSubmission?.contentType || 'article',
status: currentSubmission?.status || 'draft',
documents: {
mainArticle: {
primaryLanguage: 'en',
versions: [{
language: 'en',
content: currentArticle.content,
wordCount: currentArticle.content?.split(/\s+/).length || 0,
translatedBy: 'manual',
approved: true
}]
}
}
};
// Add cover letter if present
if (coverLetter.trim()) {
submissionData.documents.coverLetter = {
primaryLanguage: 'en',
versions: [{
language: 'en',
content: coverLetter,
wordCount: coverLetter.split(/\s+/).length,
translatedBy: 'manual',
approved: true
}]
};
}
// Add author bio if present
if (authorBio.trim()) {
submissionData.documents.authorBio = {
primaryLanguage: 'en',
versions: [{
language: 'en',
content: authorBio,
wordCount: authorBio.split(/\s+/).length,
translatedBy: 'manual',
approved: true
}]
};
}
// Add technical brief if present
if (technicalBrief.trim()) {
submissionData.documents.technicalBrief = {
primaryLanguage: 'en',
versions: [{
language: 'en',
content: technicalBrief,
wordCount: technicalBrief.split(/\s+/).length,
translatedBy: 'manual',
approved: true
}]
};
}
// Save to server
const url = currentSubmission?._id
? `/api/submissions/${currentSubmission._id}`
: '/api/submissions';
const method = currentSubmission?._id ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(submissionData)
});
if (!response.ok) throw new Error('Failed to save submission');
const savedSubmission = await response.json();
currentSubmission = savedSubmission;
const statusEl = document.getElementById('modal-status');
statusEl.textContent = '✓ Saved successfully';
statusEl.className = 'text-sm text-green-600';
setTimeout(() => {
statusEl.textContent = '';
}, 3000);
} catch (error) {
console.error('Error saving submission:', error);
const statusEl = document.getElementById('modal-status');
statusEl.textContent = '✗ Failed to save';
statusEl.className = 'text-sm text-red-600';
}
}
/**
* Export package
*/
async function exportPackage(format) {
if (!currentSubmission) {
alert('No submission package to export');
return;
}
try {
const response = await fetch(`/api/submissions/${currentSubmission._id}/export?format=${format}`);
if (!response.ok) throw new Error('Export failed');
if (format === 'json') {
const data = await response.json();
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${currentSubmission.publicationId}-package.json`;
a.click();
URL.revokeObjectURL(url);
} else if (format === 'text') {
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${currentSubmission.publicationId}-documents.zip`;
a.click();
URL.revokeObjectURL(url);
}
} catch (error) {
console.error('Error exporting package:', error);
alert('Failed to export package. Please try again.');
}
}
/**
* Copy cover letter to clipboard
*/
async function copyToClipboard() {
const coverLetter = document.getElementById('doc-coverLetter')?.value;
if (!coverLetter) {
alert('No cover letter to copy');
return;
}
try {
await navigator.clipboard.writeText(coverLetter);
const statusEl = document.getElementById('modal-status');
statusEl.textContent = '✓ Copied to clipboard';
statusEl.className = 'text-sm text-green-600';
setTimeout(() => {
statusEl.textContent = '';
}, 3000);
} catch (error) {
console.error('Error copying to clipboard:', error);
alert('Failed to copy to clipboard');
}
}
/**
* Utility: Escape HTML
*/
function escapeHtml(unsafe) {
if (!unsafe) return '';
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
/**
* Utility: Get status color class
*/
function getStatusColor(status) {
const colors = {
'ready': 'text-green-600',
'submitted': 'text-blue-600',
'published': 'text-purple-600',
'draft': 'text-gray-600'
};
return colors[status] || 'text-gray-600';
}
// Initialize modal on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createEnhancedSubmissionModal);
} else {
createEnhancedSubmissionModal();
}

View file

@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Koha — Reciprocal Support | Tractatus AI Safety</title>
<meta name="description" content="Join a relationship of mutual support for AI safety. Koha is reciprocal giving that maintains community bonds — your contribution sustains this work; our work serves you and the commons.">
<link rel="stylesheet" href="/css/tailwind.css?v=1761163813">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=1761163813">
<link rel="stylesheet" href="/css/tailwind.css?v=0.1.0.1761251931745">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=0.1.0.1761251931745">
<style>
.gradient-text { background: linear-gradient(120deg, #3b82f6 0%, #8b5cf6 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
.skip-link { position: absolute; left: -9999px; }
@ -51,7 +51,7 @@
<a href="#main-content" class="skip-link">Skip to main content</a>
<!-- Navigation (injected by navbar.js) -->
<script src="/js/components/navbar.js?v=1761163813"></script>
<script src="/js/components/navbar.js?v=0.1.0.1761251931745"></script>
<!-- Main Content -->
<main id="main-content" class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
@ -380,17 +380,17 @@
</main>
<!-- Footer -->
<script src="/js/components/footer.js?v=1761163813"></script>
<script src="/js/components/footer.js?v=0.1.0.1761251931745"></script>
<!-- Currency utilities and selector -->
<script src="/js/utils/currency.js?v=1761163813"></script>
<script src="/js/components/currency-selector.js?v=1761163813"></script>
<script src="/js/utils/currency.js?v=0.1.0.1761251931745"></script>
<script src="/js/components/currency-selector.js?v=0.1.0.1761251931745"></script>
<!-- Donation form functionality -->
<script src="/js/koha-donation.js?v=1761163813"></script>
<script src="/js/koha-donation.js?v=0.1.0.1761251931745"></script>
<!-- Internationalization -->
<script src="/js/i18n-simple.js?v=1761163813"></script>
<script src="/js/components/language-selector.js?v=1761163813"></script>
<script src="/js/i18n-simple.js?v=0.1.0.1761251931745"></script>
<script src="/js/components/language-selector.js?v=0.1.0.1761251931745"></script>
</body>
</html>

View file

@ -17,9 +17,9 @@
<link rel="apple-touch-icon" href="/images/tractatus-icon-new.svg">
<link rel="icon" type="image/svg+xml" href="/favicon-new.svg">
<link rel="stylesheet" href="/css/fonts.css?v=1761163813">
<link rel="stylesheet" href="/css/tailwind.css?v=1761163813">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=1761163813">
<link rel="stylesheet" href="/css/fonts.css?v=0.1.0.1761251931745">
<link rel="stylesheet" href="/css/tailwind.css?v=0.1.0.1761251931745">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=0.1.0.1761251931745">
<style>
.hover-lift { transition: all 0.3s ease; }
.hover-lift:hover { transform: translateY(-2px); }
@ -63,7 +63,7 @@
<a href="#main-content" class="skip-link">Skip to main content</a>
<script src="/js/components/navbar.js?v=1761163813"></script>
<script src="/js/components/navbar.js?v=0.1.0.1761251931745"></script>
<!-- Breadcrumb Navigation -->
<nav class="bg-gray-50 border-b border-gray-200 py-3" aria-label="Breadcrumb">
@ -605,20 +605,20 @@
<!-- Footer -->
<!-- Internationalization (must load first for footer translations) -->
<script src="/js/i18n-simple.js?v=1761163813"></script>
<script src="/js/components/language-selector.js?v=1761163813"></script>
<script src="/js/i18n-simple.js?v=0.1.0.1761251931745"></script>
<script src="/js/components/language-selector.js?v=0.1.0.1761251931745"></script>
<!-- Scroll Animations (Phase 3) -->
<script src="/js/scroll-animations.js?v=1761163813"></script>
<script src="/js/scroll-animations.js?v=0.1.0.1761251931745"></script>
<!-- Page Transitions (Phase 3) -->
<script src="/js/page-transitions.js?v=1761163813"></script>
<script src="/js/page-transitions.js?v=0.1.0.1761251931745"></script>
<!-- Version Management & PWA -->
<script src="/js/version-manager.js?v=1761163813"></script>
<script src="/js/leader-page.js?v=1761163813"></script>
<script src="/js/version-manager.js?v=0.1.0.1761251931745"></script>
<script src="/js/leader-page.js?v=0.1.0.1761251931745"></script>
<!-- Footer Component -->
<script src="/js/components/footer.js?v=1761163813"></script>
<script src="/js/components/footer.js?v=0.1.0.1761251931745"></script>
</body>
</html>

View file

@ -4,9 +4,9 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Media Inquiry | Tractatus AI Safety</title>
<link rel="stylesheet" href="/css/fonts.css?v=1761163813">
<link rel="stylesheet" href="/css/tailwind.css?v=1761163813">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=1761163813">
<link rel="stylesheet" href="/css/fonts.css?v=0.1.0.1761251931745">
<link rel="stylesheet" href="/css/tailwind.css?v=0.1.0.1761251931745">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=0.1.0.1761251931745">
<style>
.form-group { margin-bottom: 1.5rem; }
.form-label {
@ -68,7 +68,7 @@
<a href="#main-content" class="skip-link">Skip to main content</a>
<!-- Navigation (injected by navbar.js) -->
<script src="/js/components/navbar.js?v=1761163813"></script>
<script src="/js/components/navbar.js?v=0.1.0.1761251931745"></script>
<!-- Main Content -->
<main id="main-content" class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
@ -171,10 +171,10 @@
</main>
<!-- Footer -->
<script src="/js/media-inquiry.js?v=1761163813"></script>
<script src="/js/media-inquiry.js?v=0.1.0.1761251931745"></script>
<!-- Footer Component -->
<script src="/js/components/footer.js?v=1761163813"></script>
<script src="/js/components/footer.js?v=0.1.0.1761251931745"></script>
</body>
</html>

View file

@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title data-i18n="meta.title">Privacy Policy | Tractatus AI Safety Framework</title>
<meta name="description" content="Privacy policy for the Tractatus AI Safety Framework. Learn how we collect, use, and protect your data." data-i18n="meta.description">
<link rel="stylesheet" href="/css/tailwind.css?v=1761163813">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=1761163813">
<link rel="stylesheet" href="/css/tailwind.css?v=0.1.0.1761251931745">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=0.1.0.1761251931745">
<style>
.skip-link { position: absolute; left: -9999px; }
.skip-link:focus { left: 0; z-index: 100; background: white; padding: 1rem; }
@ -26,11 +26,11 @@
<a href="#main-content" class="skip-link">Skip to main content</a>
<!-- Navigation (injected by navbar.js) -->
<script src="/js/components/navbar.js?v=1761163813"></script>
<script src="/js/components/navbar.js?v=0.1.0.1761251931745"></script>
<!-- i18n Support -->
<script src="/js/i18n-simple.js?v=1761163813"></script>
<script src="/js/components/language-selector.js?v=1761163813"></script>
<script src="/js/i18n-simple.js?v=0.1.0.1761251931745"></script>
<script src="/js/components/language-selector.js?v=0.1.0.1761251931745"></script>
<!-- Main Content -->
<main id="main-content" class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
@ -246,7 +246,7 @@
</main>
<!-- Footer -->
<script src="/js/components/footer.js?v=1761163813"></script>
<script src="/js/components/footer.js?v=0.1.0.1761251931745"></script>
</body>
</html>

View file

@ -17,9 +17,9 @@
<link rel="apple-touch-icon" href="/images/tractatus-icon-new.svg">
<link rel="icon" type="image/svg+xml" href="/favicon-new.svg">
<link rel="stylesheet" href="/css/fonts.css?v=1761163813">
<link rel="stylesheet" href="/css/tailwind.css?v=1761163813">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=1761163813">
<link rel="stylesheet" href="/css/fonts.css?v=0.1.0.1761251931745">
<link rel="stylesheet" href="/css/tailwind.css?v=0.1.0.1761251931745">
<link rel="stylesheet" href="/css/tractatus-theme.min.css?v=0.1.0.1761251931745">
<style>
.skip-link { position: absolute; left: -9999px; }
.skip-link:focus { left: 0; z-index: 100; background: white; padding: 1rem; }
@ -69,7 +69,7 @@
</div>
</noscript>
<script src="/js/components/navbar.js?v=1761163813"></script>
<script src="/js/components/navbar.js?v=0.1.0.1761251931745"></script>
<!-- Breadcrumb Navigation -->
<nav class="bg-gray-50 border-b border-gray-200 py-3" aria-label="Breadcrumb">
@ -611,20 +611,20 @@
<!-- Footer -->
<!-- Internationalization (must load first for footer translations) -->
<script src="/js/i18n-simple.js?v=1761163813"></script>
<script src="/js/components/language-selector.js?v=1761163813"></script>
<script src="/js/i18n-simple.js?v=0.1.0.1761251931745"></script>
<script src="/js/components/language-selector.js?v=0.1.0.1761251931745"></script>
<!-- Scroll Animations (Phase 3) -->
<script src="/js/scroll-animations.js?v=1761163813"></script>
<script src="/js/scroll-animations.js?v=0.1.0.1761251931745"></script>
<!-- Page Transitions (Phase 3) -->
<script src="/js/page-transitions.js?v=1761163813"></script>
<script src="/js/page-transitions.js?v=0.1.0.1761251931745"></script>
<!-- Version Management & PWA -->
<script src="/js/version-manager.js?v=1761163813"></script>
<script src="/js/researcher-page.js?v=1761163813"></script>
<script src="/js/version-manager.js?v=0.1.0.1761251931745"></script>
<script src="/js/researcher-page.js?v=0.1.0.1761251931745"></script>
<!-- Footer Component -->
<script src="/js/components/footer.js?v=1761163813"></script>
<script src="/js/components/footer.js?v=0.1.0.1761251931745"></script>
</body>
</html>

View file

@ -5,7 +5,7 @@
* - PWA functionality
*/
const CACHE_VERSION = '1.8.4';
const CACHE_VERSION = '0.1.1';
const CACHE_NAME = `tractatus-v${CACHE_VERSION}`;
const VERSION_CHECK_INTERVAL = 3600000; // 1 hour in milliseconds

View file

@ -1,14 +1,15 @@
{
"version": "1.8.4",
"buildDate": "2025-10-24T09:45:00Z",
"version": "0.1.1",
"buildDate": "2025-10-23T20:38:51.751Z",
"changelog": [
"Blog Validation: ✨ NEW! Manage Submission modal for tracking publication submissions",
"Blog Validation: Track submission packages (cover letters, pitch emails, author bios)",
"Blog Validation: Progress indicators and automated checklist saving",
"Blog Validation: Integration with 22 ranked publication targets",
"API: New submission tracking endpoints (GET/PUT /api/blog/:id/submissions)",
"Backend: SubmissionTracking model for managing publication workflows",
"Cache: Updated cache-busting and service worker version"
"Blog Validation: ✨ ENHANCED! World-class submission modal with tabbed interface",
"Blog Validation: Overview tab with article preview and progress tracking",
"Blog Validation: Documents tab with live word counts and auto-save",
"Blog Validation: Validation tab with publication-specific rules and export",
"Blog Validation: CSP-compliant architecture with event delegation",
"API: New endpoints - by-blog-post lookup, update submission, export package",
"Backend: Multilingual document versioning support",
"Cache: Service worker v1.8.5 - FORCE REFRESH for new modal"
],
"forceUpdate": true,
"minVersion": "1.8.0"

View file

@ -0,0 +1,71 @@
#!/usr/bin/env node
/**
* Add Cache Version Enforcement Instruction
*
* Creates inst_075: MANDATORY cache version updates for JavaScript changes
*/
const fs = require('fs');
const path = require('path');
const INSTRUCTION_HISTORY_PATH = path.join(__dirname, '../.claude/instruction-history.json');
const instruction = {
id: 'inst_075',
instruction: 'MANDATORY: After modifying ANY JavaScript file in public/js/, you MUST run `node scripts/update-cache-version.js` to update service worker and version.json. This is NON-NEGOTIABLE.',
category: 'SYSTEM',
persistence: 'HIGH',
quadrant: 'rules',
context: {
rationale: 'Browser caching WILL NOT update without service worker version bump. Users will see stale JavaScript and experience broken functionality.',
enforcement: 'File write hook should WARN if .js files modified without subsequent cache version update in same session',
workflow: [
'1. Modify .js file(s)',
'2. IMMEDIATELY run: node scripts/update-cache-version.js',
'3. Verify: git diff shows version.json, service-worker.js, and HTML files updated',
'4. Commit ALL changes together'
],
consequences: 'Skipping this step causes: Production outages, stale cache bugs, user frustration, rollback required'
},
examples: [
{
scenario: 'Modified submission-modal-enhanced.js',
correct: 'Edit file → Run update-cache-version.js → Commit all changes',
incorrect: 'Edit file → Commit only .js file → Deploy (USERS GET STALE CACHE)'
}
],
relatedInstructions: ['inst_038'],
createdAt: new Date().toISOString(),
createdBy: 'cache-enforcement-setup',
lastValidated: new Date().toISOString()
};
function addInstruction() {
let history = { instructions: [] };
if (fs.existsSync(INSTRUCTION_HISTORY_PATH)) {
history = JSON.parse(fs.readFileSync(INSTRUCTION_HISTORY_PATH, 'utf8'));
}
// Check if already exists
const existing = history.instructions.find(i => i.id === 'inst_075');
if (existing) {
console.log('⚠️ inst_075 already exists. Updating...');
Object.assign(existing, instruction);
} else {
history.instructions.push(instruction);
}
fs.writeFileSync(INSTRUCTION_HISTORY_PATH, JSON.stringify(history, null, 2));
console.log('\n✅ Instruction added: inst_075');
console.log('\n📋 Instruction Details:');
console.log(` ID: ${instruction.id}`);
console.log(` Category: ${instruction.category}`);
console.log(` Persistence: ${instruction.persistence}`);
console.log(` Instruction: ${instruction.instruction}`);
console.log('\n⚠ This instruction is now ACTIVE and will be enforced by framework.\n');
}
addInstruction();

View file

@ -0,0 +1,28 @@
#!/usr/bin/env node
require('dotenv').config();
const mongoose = require('mongoose');
const SubmissionTracking = require('../src/models/SubmissionTracking.model');
const BlogPost = require('../src/models/BlogPost.model');
async function check() {
await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/tractatus_dev');
const submissions = await SubmissionTracking.find({})
.sort({ createdAt: -1 });
console.log(`\n=== SUBMISSION PACKAGES (${submissions.length}) ===\n`);
submissions.forEach((s, i) => {
console.log(`${i + 1}. ${s.publicationName}`);
console.log(` Blog Post ID: ${s.blogPostId || 'N/A'}`);
console.log(` Status: ${s.status}`);
console.log(` ID: ${s._id}`);
console.log(` Has coverLetter: ${s.documents?.coverLetter ? 'Yes' : 'No'}`);
console.log(` Has mainArticle: ${s.documents?.mainArticle ? 'Yes' : 'No'}`);
console.log(` Has authorBio: ${s.documents?.authorBio ? 'Yes' : 'No'}`);
console.log(` Has technicalBrief: ${s.documents?.technicalBrief ? 'Yes' : 'No'}\n`);
});
await mongoose.connection.close();
}
check().catch(console.error);

View file

@ -27,7 +27,41 @@ echo -e "${YELLOW} TRACTATUS FULL PROJECT DEPLOYMENT (SAFE MODE)${NC}"
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
echo -e "${GREEN}[1/4] PRE-DEPLOYMENT CHECKS${NC}"
echo -e "${GREEN}[1/5] CACHE VERSION UPDATE (MANDATORY)${NC}"
echo ""
# CRITICAL: Check if JavaScript files changed since last deployment
CHANGED_JS=$(git diff --name-only HEAD~1 2>/dev/null | grep "public/js/.*\.js$" || true)
if [ ! -z "$CHANGED_JS" ]; then
echo -e "${YELLOW}⚠ JavaScript files changed since last commit:${NC}"
echo "$CHANGED_JS" | sed 's/^/ - /'
echo ""
echo -e "${YELLOW}Running cache version update (MANDATORY)...${NC}"
# Run cache version update
cd "$PROJECT_ROOT"
node scripts/update-cache-version.js
echo ""
echo -e "${GREEN}✓ Cache version updated${NC}"
echo ""
echo -e "${YELLOW}⚠ IMPORTANT: Uncommitted changes detected!${NC}"
echo "Cache version files have been updated. You should:"
echo " 1. Review changes: git diff"
echo " 2. Commit: git add -A && git commit -m 'chore: bump cache version'"
echo " 3. Re-run deployment"
echo ""
read -p "Continue deployment with uncommitted cache changes? (yes/NO): " continue_uncommitted
if [ "$continue_uncommitted" != "yes" ]; then
echo "Deployment cancelled. Commit cache version changes first."
exit 1
fi
else
echo -e "${GREEN}✓ No JavaScript files changed - cache version update not required${NC}"
fi
echo ""
echo -e "${GREEN}[2/5] PRE-DEPLOYMENT CHECKS${NC}"
echo ""
# Check if .rsyncignore exists
@ -75,14 +109,14 @@ fi
# Show excluded patterns
echo ""
echo -e "${GREEN}[2/4] SECURITY CHECK${NC}"
echo -e "${GREEN}[3/5] SECURITY CHECK${NC}"
echo "Excluded patterns from .rsyncignore:"
head -20 "$PROJECT_ROOT/.rsyncignore" | grep -v "^#" | grep -v "^$" | sed 's/^/ - /'
echo " ... (see .rsyncignore for full list)"
echo ""
# Confirm deployment
echo -e "${GREEN}[3/4] DEPLOYMENT CONFIRMATION${NC}"
echo -e "${GREEN}[4/5] DEPLOYMENT CONFIRMATION${NC}"
echo -e "${YELLOW}WARNING: This will sync the ENTIRE project directory${NC}"
echo "Source: $PROJECT_ROOT"
echo "Destination: $REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH"
@ -117,7 +151,7 @@ fi
# Actual deployment
echo ""
echo -e "${GREEN}[4/4] DEPLOYING TO PRODUCTION${NC}"
echo -e "${GREEN}[5/5] DEPLOYING TO PRODUCTION${NC}"
rsync -avz --delete \
-e "ssh -i $DEPLOY_KEY" \
--exclude-from="$PROJECT_ROOT/.rsyncignore" \

View file

@ -3,20 +3,37 @@
/**
* Update Cache Version - Unified Cache Busting
*
* Updates all HTML files with a consistent cache-busting version string.
* CRITICAL: Run this script EVERY TIME JavaScript files are modified!
*
* Updates:
* 1. All HTML files with ?v= cache-busting parameters
* 2. public/service-worker.js CACHE_VERSION constant
* 3. public/version.json with new version and changelog
*
* Format: v={package.version}.{timestamp}
* Example: v=0.1.0.1760201234
*
* This ensures all assets (CSS, JS) are loaded with the same version,
* solving the inconsistent cache busting problem.
* This ensures:
* - Browser cache is invalidated
* - Service worker forces refresh
* - Version tracking is updated
*/
const fs = require('fs');
const path = require('path');
const packageJson = require('../package.json');
// Parse semantic version from package.json
const [major, minor, patch] = packageJson.version.split('.').map(Number);
// Generate cache version: package version + timestamp
const CACHE_VERSION = `${packageJson.version}.${Date.now()}`;
const timestamp = Date.now();
const CACHE_VERSION = `${packageJson.version}.${timestamp}`;
// Bump patch version for version.json
const NEW_SEMVER = `${major}.${minor}.${patch + 1}`;
const VERSION_FILE = path.join(__dirname, '../public/version.json');
const SERVICE_WORKER_FILE = path.join(__dirname, '../public/service-worker.js');
// HTML files to update (relative to project root)
const HTML_FILES = [
@ -35,7 +52,9 @@ const HTML_FILES = [
'public/media-inquiry.html',
'public/case-submission.html',
'public/koha.html',
'public/check-version.html'
'public/check-version.html',
'public/admin/blog-curation.html',
'public/admin/admin-dashboard.html'
];
/**
@ -77,19 +96,83 @@ function updateCacheVersion(filePath) {
}
}
/**
* Update service worker CACHE_VERSION
*/
function updateServiceWorker() {
try {
let content = fs.readFileSync(SERVICE_WORKER_FILE, 'utf8');
const original = content;
// Update CACHE_VERSION constant
content = content.replace(
/const CACHE_VERSION = '[^']+';/,
`const CACHE_VERSION = '${NEW_SEMVER}';`
);
if (content !== original) {
fs.writeFileSync(SERVICE_WORKER_FILE, content);
console.log(`✅ service-worker.js: Updated CACHE_VERSION to ${NEW_SEMVER}`);
return true;
}
return false;
} catch (error) {
console.error(`❌ Error updating service-worker.js:`, error.message);
return false;
}
}
/**
* Update version.json
*/
function updateVersionJson() {
try {
const versionData = JSON.parse(fs.readFileSync(VERSION_FILE, 'utf8'));
versionData.version = NEW_SEMVER;
versionData.buildDate = new Date().toISOString();
versionData.forceUpdate = true;
// Preserve existing changelog
if (!versionData.changelog) {
versionData.changelog = ['Cache version update - JavaScript files modified'];
}
fs.writeFileSync(VERSION_FILE, JSON.stringify(versionData, null, 2) + '\n');
console.log(`✅ version.json: Updated to ${NEW_SEMVER}`);
return true;
} catch (error) {
console.error(`❌ Error updating version.json:`, error.message);
return false;
}
}
/**
* Main execution
*/
function main() {
console.log('');
console.log('═'.repeat(70));
console.log(' Tractatus - Cache Version Update');
console.log(' Tractatus - Cache Version Update (CRITICAL FOR .JS CHANGES)');
console.log('═'.repeat(70));
console.log('');
console.log(`📦 Package version: ${packageJson.version}`);
console.log(`🔄 New cache version: ${CACHE_VERSION}`);
console.log(`🔄 New semantic version: ${NEW_SEMVER}`);
console.log(`🔄 New cache-bust version: ${CACHE_VERSION}`);
console.log('');
// Step 1: Update service worker
console.log('Step 1: Updating service worker...');
updateServiceWorker();
console.log('');
// Step 2: Update version.json
console.log('Step 2: Updating version.json...');
updateVersionJson();
console.log('');
// Step 3: Update HTML cache parameters
console.log('Step 3: Updating HTML cache parameters...');
let updatedCount = 0;
let totalFiles = 0;
@ -102,16 +185,21 @@ function main() {
console.log('');
console.log('═'.repeat(70));
console.log(` Summary: ${updatedCount}/${totalFiles} files updated`);
console.log(` Summary: ${updatedCount}/${totalFiles} HTML files updated`);
console.log('═'.repeat(70));
console.log('');
if (updatedCount > 0) {
console.log('✅ Cache version updated successfully!');
console.log(` All assets will now use: ?v=${CACHE_VERSION}`);
} else {
console.log(' No files needed updating');
}
console.log('✅ Cache version update complete!');
console.log('');
console.log('📝 Files modified:');
console.log(' - public/service-worker.js (CACHE_VERSION)');
console.log(' - public/version.json (version + buildDate)');
console.log(` - ${updatedCount} HTML files (?v= parameters)`);
console.log('');
console.log('⚠️ NEXT STEPS:');
console.log(' 1. Review changes: git diff');
console.log(' 2. Commit: git add -A && git commit -m "chore: bump cache version"');
console.log(' 3. Deploy to production');
console.log('');
}

View file

@ -269,6 +269,144 @@ async function getSubmissionsByPublication(req, res) {
}
}
/**
* GET /api/submissions/by-blog-post/:blogPostId
* Get submission by blog post ID
*/
async function getSubmissionByBlogPost(req, res) {
try {
const { blogPostId } = req.params;
const submission = await SubmissionTracking.findOne({ blogPostId })
.populate('blogPostId', 'title slug content')
.populate('createdBy', 'email')
.populate('lastUpdatedBy', 'email');
if (!submission) {
return res.status(404).json({
success: false,
error: 'No submission found for this blog post'
});
}
res.json({
success: true,
data: submission
});
} catch (error) {
console.error('[Submissions] Get by blog post error:', error);
res.status(500).json({
success: false,
error: 'Failed to fetch submission'
});
}
}
/**
* PUT /api/submissions/:id
* Update submission entry
*/
async function updateSubmission(req, res) {
try {
const { id } = req.params;
const updateData = req.body;
const submission = await SubmissionTracking.findById(id);
if (!submission) {
return res.status(404).json({
success: false,
error: 'Submission not found'
});
}
// Update documents if provided
if (updateData.documents) {
for (const [docType, docData] of Object.entries(updateData.documents)) {
if (docData.versions && docData.versions.length > 0) {
const version = docData.versions[0];
await submission.setDocumentVersion(
docType,
version.language,
version.content,
{
translatedBy: version.translatedBy,
approved: version.approved
}
);
}
}
delete updateData.documents; // Remove from update data since it's handled separately
}
// Update other fields
Object.assign(submission, updateData);
submission.lastUpdatedBy = req.user._id;
await submission.save();
res.json({
success: true,
data: submission
});
} catch (error) {
console.error('[Submissions] Update submission error:', error);
res.status(500).json({
success: false,
error: 'Failed to update submission'
});
}
}
/**
* GET /api/submissions/:id/export
* Export submission package
*/
async function exportSubmission(req, res) {
try {
const { id } = req.params;
const { format = 'json' } = req.query;
const submission = await SubmissionTracking.findById(id)
.populate('blogPostId', 'title content');
if (!submission) {
return res.status(404).json({
success: false,
error: 'Submission not found'
});
}
const language = req.query.language || 'en';
const packageData = submission.exportPackage(language);
if (format === 'json') {
res.json({
success: true,
data: packageData
});
} else if (format === 'text') {
// For now, return JSON with instructions
// In future, could zip individual text files
res.json({
success: true,
data: packageData,
note: 'Text export feature coming soon. Please use JSON export for now.'
});
} else {
res.status(400).json({
success: false,
error: 'Invalid format. Use "json" or "text"'
});
}
} catch (error) {
console.error('[Submissions] Export submission error:', error);
res.status(500).json({
success: false,
error: 'Failed to export submission'
});
}
}
/**
* DELETE /api/submissions/:id
* Delete submission tracking entry
@ -302,9 +440,12 @@ module.exports = {
createSubmission,
getSubmissions,
getSubmissionById,
getSubmissionByBlogPost,
updateSubmission,
updateSubmissionStatus,
addSubmissionNote,
getSubmissionStatistics,
getSubmissionsByPublication,
exportSubmission,
deleteSubmission
};

View file

@ -37,12 +37,30 @@ router.get('/statistics', submissionsController.getSubmissionStatistics);
*/
router.get('/publication/:publicationId', submissionsController.getSubmissionsByPublication);
/**
* GET /api/submissions/by-blog-post/:blogPostId
* Get submission by blog post ID
*/
router.get('/by-blog-post/:blogPostId', submissionsController.getSubmissionByBlogPost);
/**
* GET /api/submissions/:id/export
* Export submission package
*/
router.get('/:id/export', submissionsController.exportSubmission);
/**
* GET /api/submissions/:id
* Get specific submission by ID
*/
router.get('/:id', submissionsController.getSubmissionById);
/**
* PUT /api/submissions/:id
* Update submission entry
*/
router.put('/:id', submissionsController.updateSubmission);
/**
* PUT /api/submissions/:id/status
* Update submission status