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:
parent
2298d36bed
commit
971690bb64
27 changed files with 1657 additions and 165 deletions
336
CACHE_MANAGEMENT_ENFORCEMENT.md
Normal file
336
CACHE_MANAGEMENT_ENFORCEMENT.md
Normal 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
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
775
public/js/admin/submission-modal-enhanced.js
Normal file
775
public/js/admin/submission-modal-enhanced.js
Normal 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">
|
||||
×
|
||||
</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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
71
scripts/add-cache-enforcement-instruction.js
Normal file
71
scripts/add-cache-enforcement-instruction.js
Normal 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();
|
||||
28
scripts/check-submissions.js
Normal file
28
scripts/check-submissions.js
Normal 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);
|
||||
|
|
@ -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" \
|
||||
|
|
|
|||
|
|
@ -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('');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue