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 2298d36bed
commit 971690bb64
27 changed files with 1657 additions and 165 deletions

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