tractatus/public/js/admin/blog-validation.js
TheFlow f7d0b68d39 fix(admin): force fresh API requests to prevent cached 500 errors
- Add cache: 'no-store' to all apiCall functions in admin JS files
- Prevents browser fetch cache from serving stale error responses
- Addresses submissions endpoint 500 errors that weren't appearing in server logs
- Killed duplicate server process (PID 1583625)
- Added debug logging to submissions controller
- Files modified: blog-validation.js, blog-curation.js, blog-curation-enhanced.js
2025-10-24 11:02:43 +13:00

861 lines
33 KiB
JavaScript

/**
* Blog Article Validation
* Two-level validation: Content Similarity + Title Similarity
*/
// Helper to safely convert ObjectId to string
function toStringId(id) {
if (!id) {
console.warn('[toStringId] Received empty id');
return '';
}
if (typeof id === 'string') return id;
// Handle Buffer object (MongoDB ObjectId as buffer)
if (typeof id === 'object' && id.buffer) {
console.log('[toStringId] Converting buffer to hex string');
const bytes = [];
for (let i = 0; i < 12; i++) {
if (id.buffer[i] !== undefined) {
bytes.push(id.buffer[i].toString(16).padStart(2, '0'));
}
}
const hexString = bytes.join('');
console.log('[toStringId] Hex string:', hexString);
return hexString;
}
// Check for other common MongoDB ObjectId representations
if (typeof id === 'object') {
if (id.$oid) return id.$oid;
if (id.id) return id.id;
if (id._id) return id._id;
}
const result = String(id);
console.log('[toStringId] Fallback String():', result);
return result;
}
// Get auth token
function getAuthToken() {
return localStorage.getItem('admin_token');
}
// API call helper
async function apiCall(endpoint, options = {}) {
const token = getAuthToken();
const defaultOptions = {
cache: 'no-store', // Force fresh requests - prevent cached 500 errors
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
};
const response = await fetch(endpoint, { ...defaultOptions, ...options });
if (response.status === 401) {
localStorage.removeItem('admin_token');
window.location.href = '/admin/login.html';
throw new Error('Unauthorized');
}
return response;
}
// Load all pending review articles
async function loadValidationArticles() {
const listDiv = document.getElementById('validation-list');
if (!listDiv) return;
listDiv.innerHTML = '<div class="px-6 py-8 text-center text-gray-500">Loading articles and submission packages...</div>';
try {
// Load blog posts with status=pending_review
const articlesResponse = await apiCall('/api/blog/admin/posts?status=pending_review&limit=100');
if (!articlesResponse.ok) {
throw new Error('Failed to load articles');
}
const articlesData = await articlesResponse.json();
const articles = articlesData.posts || [];
console.log('[Validation] Loaded articles:', articles.length);
// ALSO load standalone submission packages (those without blogPostId)
const submissionsResponse = await apiCall('/api/submissions?limit=100');
const standaloneSubmissions = [];
console.log('[Validation] Submissions response ok:', submissionsResponse.ok);
if (submissionsResponse.ok) {
const submissionsData = await submissionsResponse.json();
console.log('[Validation] Submissions data:', submissionsData);
const allSubmissions = submissionsData.data || [];
console.log('[Validation] Total submissions found:', allSubmissions.length);
// Filter for standalone submissions (no blogPostId)
allSubmissions.forEach(sub => {
console.log('[Validation] Checking submission:', sub.title, 'blogPostId:', sub.blogPostId);
if (!sub.blogPostId) {
const standaloneSub = {
_id: typeof sub._id === 'object' ? toStringId(sub._id) : sub._id.toString(),
title: sub.title,
status: 'submission_package',
publicationName: sub.publicationName,
publicationId: sub.publicationId,
wordCount: sub.wordCount,
contentType: sub.contentType,
submissionStatus: sub.status,
isStandalone: true,
submissionData: sub
};
console.log('[Validation] Adding standalone submission:', standaloneSub.title);
standaloneSubmissions.push(standaloneSub);
}
});
console.log('[Validation] Loaded standalone submissions:', standaloneSubmissions.length);
} else {
console.error('[Validation] Failed to load submissions');
}
// Combine articles and standalone submissions
const allItems = [...articles, ...standaloneSubmissions];
if (allItems.length === 0) {
listDiv.innerHTML = '<div class="px-6 py-8 text-center text-gray-500">No articles or packages pending review</div>';
return;
}
// Load submission tracking for each blog post article (not standalone submissions)
const submissionData = {};
for (const article of articles) {
const articleId = toStringId(article._id);
try {
const subResponse = await apiCall(`/api/submissions/by-blog-post/${articleId}`);
if (subResponse.ok) {
const subData = await subResponse.json();
if (subData.data) {
submissionData[articleId] = subData.data;
}
}
} catch (err) {
// Silently ignore if no submission found
}
}
// Render all items (blog posts + standalone submissions)
listDiv.innerHTML = allItems.map(item => {
const itemId = toStringId(item._id);
const isStandalone = item.isStandalone;
// For standalone submissions, use submissionData directly
const submission = isStandalone ? item.submissionData : submissionData[itemId];
// Calculate word count
const wordCount = isStandalone
? (item.wordCount || 0)
: (item.content ? item.content.split(/\s+/).length : 0);
// Calculate checklist completion
let checklistItems = 0;
let checklistCompleted = 0;
if (submission && submission.submissionPackage) {
const pkg = submission.submissionPackage;
if (pkg.coverLetter) {
checklistItems++;
if (pkg.coverLetter.completed) checklistCompleted++;
}
if (pkg.notesToEditor) {
checklistItems++;
if (pkg.notesToEditor.completed) checklistCompleted++;
}
if (pkg.authorBio) {
checklistItems++;
if (pkg.authorBio.completed) checklistCompleted++;
}
if (pkg.pitchEmail) {
checklistItems++;
if (pkg.pitchEmail.completed) checklistCompleted++;
}
}
const checklistColor = checklistCompleted === checklistItems && checklistItems > 0
? 'text-green-600'
: 'text-yellow-600';
return `
<div class="px-6 py-4 hover:bg-gray-50 border-l-4 ${submission ? 'border-l-blue-500' : 'border-l-gray-300'}" data-article-id="${itemId}">
<div class="flex justify-between items-start">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<h4 class="font-medium text-gray-900">${item.title}</h4>
${isStandalone ? `<span class="px-2 py-1 bg-purple-100 text-purple-800 text-xs rounded font-medium">STANDALONE PACKAGE</span>` : ''}
${submission ? `<span class="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded font-medium">${item.publicationName || submission.publicationName}</span>` : ''}
</div>
<div class="flex items-center gap-4 mt-2 text-xs text-gray-500">
${!isStandalone ? `<span>Slug: ${item.slug}</span>` : ''}
<span>Words: ${wordCount}</span>
${!isStandalone && item.published_at ? `<span>Created: ${new Date(item.published_at).toLocaleDateString()}</span>` : ''}
${submission ? `<span class="font-medium">${(item.contentType || submission.contentType || 'article').toUpperCase()}</span>` : ''}
</div>
${submission ? `
<div class="mt-2 flex items-center gap-2">
<span class="text-xs ${checklistColor}">
📋 Checklist: ${checklistCompleted}/${checklistItems} complete
</span>
${submission.submissionMethod ? `<span class="text-xs text-gray-500">via ${submission.submissionMethod}</span>` : ''}
</div>
` : '<div class="mt-2 text-xs text-orange-600">⚠️ No submission tracking - click "Manage Submission"</div>'}
<div class="mt-3 flex items-center gap-2">
<div class="validation-status-${itemId} inline-flex items-center gap-2">
${isStandalone
? `<span class="px-2 py-1 bg-green-100 text-green-600 text-xs rounded">✓ Submission Package Ready</span>`
: `<span class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded">Not validated yet</span>`
}
</div>
</div>
</div>
<div class="ml-4 flex items-center gap-2 flex-wrap">
<button class="manage-submission px-4 py-2 bg-green-600 text-white rounded-md text-sm hover:bg-green-700"
data-article-id="${itemId}"
data-submission-id="${submission ? submission._id : ''}">
${isStandalone ? 'View Package' : 'Manage Submission'}
</button>
${!isStandalone ? `
<button class="edit-article px-4 py-2 bg-gray-600 text-white rounded-md text-sm hover:bg-gray-700"
data-article-id="${itemId}">
Edit
</button>` : ''}
${!isStandalone ? `
<button class="validate-article px-4 py-2 bg-blue-600 text-white rounded-md text-sm hover:bg-blue-700"
data-article-id="${itemId}"
data-article-title="${item.title.replace(/"/g, '&quot;')}">
Validate` : ''}
</button>
</div>
</div>
</div>
`;
}).join('');
// Add edit handlers
listDiv.querySelectorAll('.edit-article').forEach(btn => {
btn.addEventListener('click', () => {
const articleId = btn.dataset.articleId;
openEditModal(articleId);
});
});
// Add validate handlers
listDiv.querySelectorAll('.validate-article').forEach(btn => {
btn.addEventListener('click', () => {
const articleId = btn.dataset.articleId;
const articleTitle = btn.dataset.articleTitle;
runValidation(articleId, articleTitle);
});
});
} catch (error) {
console.error('Failed to load validation articles:', error);
listDiv.innerHTML = '<div class="px-6 py-8 text-center text-red-500">Error loading articles</div>';
}
}
// Run validation on an article
async function runValidation(articleId, articleTitle) {
const statusDiv = document.querySelector(`.validation-status-${articleId}`);
const btn = document.querySelector(`.validate-article[data-article-id="${articleId}"]`);
if (!statusDiv || !btn) return;
// Update UI
btn.disabled = true;
btn.textContent = 'Validating...';
statusDiv.innerHTML = '<span class="px-2 py-1 bg-blue-100 text-blue-600 text-xs rounded">Validating...</span>';
try {
const response = await apiCall('/api/blog/validate-article', {
method: 'POST',
body: JSON.stringify({ articleId })
});
const result = await response.json();
if (response.ok && result.validation) {
const v = result.validation;
// Count passes and fails
const contentFails = v.contentChecks.filter(c => !c.pass).length;
const titleFails = v.titleChecks.filter(c => !c.pass).length;
// Build status badges
let badges = [];
// Content badge
if (contentFails === 0) {
badges.push('<span class="px-2 py-1 bg-green-100 text-green-800 text-xs rounded">✅ Content: Pass</span>');
} else {
badges.push(`<span class="px-2 py-1 bg-red-100 text-red-800 text-xs rounded">❌ Content: ${contentFails} conflict(s)</span>`);
}
// Title badge
if (titleFails === 0) {
badges.push('<span class="px-2 py-1 bg-green-100 text-green-800 text-xs rounded">✅ Title: Pass</span>');
} else {
badges.push(`<span class="px-2 py-1 bg-red-100 text-red-800 text-xs rounded">🚫 Title: ${titleFails} conflict(s)</span>`);
}
statusDiv.innerHTML = badges.join(' ');
// Show detailed modal
showValidationModal(articleTitle, result.validation);
} else {
statusDiv.innerHTML = '<span class="px-2 py-1 bg-red-100 text-red-800 text-xs rounded">Error</span>';
alert(`Validation error: ${result.message || 'Unknown error'}`);
}
} catch (error) {
console.error('Validation error:', error);
statusDiv.innerHTML = '<span class="px-2 py-1 bg-red-100 text-red-800 text-xs rounded">Error</span>';
alert(`Validation error: ${error.message}`);
} finally {
btn.disabled = false;
btn.textContent = 'Validate';
}
}
// Show validation results modal
function showValidationModal(articleTitle, validation) {
const { contentChecks, titleChecks, overallStatus, errors, warnings, summary } = validation;
const contentChecksHtml = contentChecks.length > 0 ? `
<div class="mb-6">
<h4 class="text-sm font-semibold text-gray-900 mb-3">Content Similarity Checks (${summary.contentPasses} pass, ${summary.contentFails} fail)</h4>
<div class="space-y-2">
${contentChecks.slice(0, 5).map(check => {
const bgClass = check.pass ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200';
const iconClass = check.pass ? 'text-green-600' : 'text-red-600';
const icon = check.pass ? '✅' : '❌';
return `
<div class="border ${bgClass} rounded-md p-3">
<div class="flex items-start gap-2">
<span class="${iconClass} text-lg">${icon}</span>
<div class="flex-1">
<div class="text-sm font-medium text-gray-900">vs. "${check.comparedWith}"</div>
<div class="text-xs text-gray-600 mt-1">
Similarity: ${Math.round(check.similarity * 100)}%
</div>
<div class="text-xs text-gray-700 mt-1">${check.message}</div>
</div>
</div>
</div>
`;
}).join('')}
</div>
</div>
` : '<div class="text-sm text-gray-500 mb-6">No other articles to compare</div>';
const titleChecksHtml = titleChecks.length > 0 ? `
<div class="mb-6">
<h4 class="text-sm font-semibold text-gray-900 mb-3">Title Similarity Checks (${summary.titlePasses} pass, ${summary.titleFails} fail)</h4>
<div class="space-y-2">
${titleChecks.slice(0, 5).map(check => {
const bgClass = check.pass ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200';
const iconClass = check.pass ? 'text-green-600' : 'text-red-600';
const icon = check.pass ? '✅' : '🚫';
return `
<div class="border ${bgClass} rounded-md p-3">
<div class="flex items-start gap-2">
<span class="${iconClass} text-lg">${icon}</span>
<div class="flex-1">
<div class="text-sm font-medium text-gray-900">vs. "${check.comparedWith}"</div>
<div class="text-xs text-gray-600 mt-1">
Similarity: ${Math.round(check.similarity * 100)}%
${check.sharedWords && check.sharedWords.length > 0 ? ` | Shared: ${check.sharedWords.join(', ')}` : ''}
</div>
<div class="text-xs text-gray-700 mt-1">${check.message}</div>
</div>
</div>
</div>
`;
}).join('')}
</div>
</div>
` : '<div class="text-sm text-gray-500 mb-6">No other articles to compare</div>';
const errorsHtml = errors.length > 0 ? `
<div class="bg-red-50 border border-red-200 rounded-md p-4 mb-4">
<h5 class="font-medium text-red-900 mb-2">❌ Errors - Cannot Submit</h5>
<ul class="text-sm text-red-800 list-disc list-inside space-y-1">
${errors.map(e => `<li>${e}</li>`).join('')}
</ul>
</div>
` : '';
const warningsHtml = warnings.length > 0 ? `
<div class="bg-yellow-50 border border-yellow-200 rounded-md p-4 mb-4">
<h5 class="font-medium text-yellow-900 mb-2">⚠️ Warnings</h5>
<ul class="text-sm text-yellow-800 list-disc list-inside space-y-1">
${warnings.map(w => `<li>${w}</li>`).join('')}
</ul>
</div>
` : '';
const summaryClass = overallStatus === 'pass' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800';
const summaryText = overallStatus === 'pass' ? '✅ PASS - Ready for submission' : '❌ FAIL - Revisions required';
const modal = `
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50 p-4">
<div class="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
<h3 class="text-lg font-medium text-gray-900">Validation: "${articleTitle}"</h3>
<button class="close-validation-modal text-gray-400 hover:text-gray-600">
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="flex-1 overflow-y-auto px-6 py-4">
<div class="mb-6">
<div class="flex items-center justify-between p-4 border-2 rounded-lg ${summaryClass.replace('100', '200')}">
<span class="font-semibold">${summaryText}</span>
<span class="text-sm">Checked against ${summary.totalChecks} article(s)</span>
</div>
</div>
${errorsHtml}
${warningsHtml}
${contentChecksHtml}
${titleChecksHtml}
</div>
<div class="px-6 py-4 border-t border-gray-200 flex justify-end">
<button class="close-validation-modal px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">
Close
</button>
</div>
</div>
</div>
`;
const container = document.getElementById('modal-container');
if (!container) return;
container.innerHTML = modal;
// Close modal handlers
container.querySelectorAll('.close-validation-modal').forEach(btn => {
btn.addEventListener('click', () => {
container.innerHTML = '';
});
});
}
// Open edit modal
async function openEditModal(articleId) {
const container = document.getElementById('modal-container');
container.innerHTML = '<div class="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50 p-4"><div class="text-white">Loading article...</div></div>';
try {
const response = await apiCall(`/api/blog/admin/${articleId}`);
if (!response.ok) {
throw new Error('Failed to load article');
}
const data = await response.json();
const article = data.post;
const modal = `
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50 p-4">
<div class="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col">
<div class="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
<h3 class="text-lg font-medium text-gray-900">Edit Article</h3>
<button class="close-edit-modal text-gray-400 hover:text-gray-600">
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="flex-1 overflow-y-auto px-6 py-4">
<form id="edit-article-form">
<div class="space-y-4">
<div>
<label for="edit-title" class="block text-sm font-medium text-gray-700">Title *</label>
<input type="text" id="edit-title" name="title" required
value="${article.title.replace(/"/g, '&quot;')}"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
<p class="mt-1 text-xs text-gray-500">Change this if there's a title conflict</p>
</div>
<div>
<label for="edit-slug" class="block text-sm font-medium text-gray-700">Slug *</label>
<input type="text" id="edit-slug" name="slug" required
value="${article.slug}"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
<p class="mt-1 text-xs text-gray-500">URL-friendly version (lowercase, hyphens)</p>
</div>
<div>
<label for="edit-excerpt" class="block text-sm font-medium text-gray-700">Excerpt</label>
<textarea id="edit-excerpt" name="excerpt" rows="3"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">${article.excerpt || ''}</textarea>
</div>
<div>
<label for="edit-content" class="block text-sm font-medium text-gray-700">Content *</label>
<textarea id="edit-content" name="content" rows="15" required
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 font-mono text-sm">${article.content}</textarea>
<p class="mt-1 text-xs text-gray-500">Markdown format</p>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="edit-status" class="block text-sm font-medium text-gray-700">Status</label>
<select id="edit-status" name="status"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
<option value="draft" ${article.status === 'draft' ? 'selected' : ''}>Draft</option>
<option value="pending_review" ${article.status === 'pending_review' ? 'selected' : ''}>Pending Review</option>
<option value="published" ${article.status === 'published' ? 'selected' : ''}>Published</option>
</select>
</div>
<div>
<label for="edit-tags" class="block text-sm font-medium text-gray-700">Tags</label>
<input type="text" id="edit-tags" name="tags"
value="${(article.tags || []).join(', ')}"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
<p class="mt-1 text-xs text-gray-500">Comma-separated</p>
</div>
</div>
</div>
<div id="edit-status-message" class="mt-4 text-sm"></div>
</form>
</div>
<div class="px-6 py-4 border-t border-gray-200 flex justify-between">
<button class="close-edit-modal px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">
Cancel
</button>
<button id="save-article-btn" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">
Save Changes
</button>
</div>
</div>
</div>
`;
container.innerHTML = modal;
// Close handlers
container.querySelectorAll('.close-edit-modal').forEach(btn => {
btn.addEventListener('click', () => {
container.innerHTML = '';
});
});
// Save handler
container.querySelector('#save-article-btn').addEventListener('click', () => {
saveArticle(articleId);
});
} catch (error) {
console.error('Failed to load article:', error);
container.innerHTML = '';
alert('Failed to load article: ' + error.message);
}
}
// Save article changes
async function saveArticle(articleId) {
const statusDiv = document.getElementById('edit-status-message');
const saveBtn = document.getElementById('save-article-btn');
const form = document.getElementById('edit-article-form');
const formData = new FormData(form);
const updates = {
title: formData.get('title'),
slug: formData.get('slug'),
excerpt: formData.get('excerpt'),
content: formData.get('content'),
status: formData.get('status'),
tags: formData.get('tags').split(',').map(t => t.trim()).filter(t => t)
};
saveBtn.disabled = true;
saveBtn.textContent = 'Saving...';
statusDiv.textContent = 'Saving changes...';
statusDiv.className = 'mt-4 text-sm text-blue-600';
try {
const response = await apiCall(`/api/blog/${articleId}`, {
method: 'PUT',
body: JSON.stringify(updates)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to save changes');
}
statusDiv.textContent = '✓ Changes saved successfully!';
statusDiv.className = 'mt-4 text-sm text-green-600';
// Close modal and refresh list after short delay
setTimeout(() => {
document.getElementById('modal-container').innerHTML = '';
loadValidationArticles();
}, 1000);
} catch (error) {
console.error('Save error:', error);
statusDiv.textContent = `✗ Error: ${error.message}`;
statusDiv.className = 'mt-4 text-sm text-red-600';
saveBtn.disabled = false;
saveBtn.textContent = 'Save Changes';
}
}
// Load published posts
async function loadPublishedPosts() {
const listDiv = document.getElementById('published-list');
if (!listDiv) return;
listDiv.innerHTML = '<div class="px-6 py-8 text-center text-gray-500">Loading published posts...</div>';
try {
const response = await apiCall('/api/blog/admin/posts?status=published&limit=100');
if (!response.ok) {
throw new Error('Failed to load published posts');
}
const data = await response.json();
const posts = data.posts || [];
if (posts.length === 0) {
listDiv.innerHTML = '<div class="px-6 py-8 text-center text-gray-500">No published posts found.</div>';
return;
}
listDiv.innerHTML = posts.map(post => {
const articleId = toStringId(post._id);
const wordCount = post.content ? post.content.split(/\s+/).length : 0;
const excerpt = post.excerpt || (post.content ? post.content.substring(0, 150) + '...' : 'No excerpt');
const tags = (post.tags || []).slice(0, 3).map(tag =>
`<span class="inline-block bg-gray-100 text-gray-700 text-xs px-2 py-1 rounded">${tag}</span>`
).join(' ');
return `
<div class="px-6 py-4 hover:bg-gray-50">
<div class="flex items-start justify-between">
<div class="flex-1 min-w-0">
<h4 class="font-medium text-gray-900">${post.title}</h4>
<p class="mt-1 text-sm text-gray-600">${excerpt}</p>
<div class="mt-2 flex items-center gap-4 text-xs text-gray-500">
<span>${wordCount} words</span>
<span>Slug: ${post.slug}</span>
${post.publishedAt ? `<span>Published: ${new Date(post.publishedAt).toLocaleDateString()}</span>` : ''}
</div>
${tags ? `<div class="mt-2 flex flex-wrap gap-1">${tags}</div>` : ''}
</div>
<div class="ml-4 flex flex-col gap-2">
<button class="read-article text-sm text-blue-600 hover:text-blue-800" data-article-id="${articleId}">
Read
</button>
<a href="https://agenticgovernance.digital/blog/${post.slug}" target="_blank"
class="text-sm text-gray-600 hover:text-gray-800">
View Live
</a>
</div>
</div>
</div>
`;
}).join('');
// Attach read button handlers
document.querySelectorAll('.read-article').forEach(btn => {
btn.addEventListener('click', async (e) => {
const articleId = e.target.dataset.articleId;
const post = posts.find(p => toStringId(p._id) === articleId);
if (post) {
showReadModal(post);
}
});
});
} catch (error) {
console.error('Failed to load published posts:', error);
listDiv.innerHTML = '<div class="px-6 py-8 text-center text-red-500">Failed to load published posts. Please try again.</div>';
}
}
// Show read modal for a post
function showReadModal(post) {
const modal = document.createElement('div');
modal.className = 'fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50';
modal.innerHTML = `
<div class="relative top-20 mx-auto p-5 border w-11/12 max-w-4xl shadow-lg rounded-md bg-white">
<div class="flex justify-between items-start mb-4">
<h3 class="text-2xl font-bold text-gray-900">${post.title}</h3>
<button class="close-modal text-gray-400 hover:text-gray-600 text-3xl font-bold">&times;</button>
</div>
<div class="mb-4 text-sm text-gray-600">
<div class="flex items-center gap-4">
<span>Slug: ${post.slug}</span>
${post.publishedAt ? `<span>Published: ${new Date(post.publishedAt).toLocaleDateString()}</span>` : ''}
</div>
${post.tags && post.tags.length > 0 ? `
<div class="mt-2 flex flex-wrap gap-1">
${post.tags.map(tag => `<span class="bg-gray-100 text-gray-700 text-xs px-2 py-1 rounded">${tag}</span>`).join(' ')}
</div>
` : ''}
</div>
${post.excerpt ? `
<div class="mb-4 p-4 bg-gray-50 rounded">
<p class="text-sm italic text-gray-700">${post.excerpt}</p>
</div>
` : ''}
<div class="prose max-w-none">
${post.content || '<p class="text-gray-500">No content available.</p>'}
</div>
<div class="mt-6 flex justify-end gap-3">
<a href="https://agenticgovernance.digital/blog/${post.slug}" target="_blank"
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">
View Live
</a>
<button class="close-modal px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300">
Close
</button>
</div>
</div>
`;
document.body.appendChild(modal);
// Close modal handlers
modal.querySelectorAll('.close-modal').forEach(btn => {
btn.addEventListener('click', () => modal.remove());
});
// Close on background click
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.remove();
}
});
}
// Initialize section navigation
function initSectionNavigation() {
const tabs = document.querySelectorAll('.section-tab');
const sections = {
validation: document.getElementById('validation-section'),
draft: document.getElementById('draft-section'),
queue: document.getElementById('queue-section'),
guidelines: document.getElementById('guidelines-section'),
published: document.getElementById('published-section')
};
tabs.forEach(tab => {
tab.addEventListener('click', () => {
const targetSection = tab.dataset.section;
// Update tab styles
tabs.forEach(t => {
t.classList.remove('border-blue-500', 'text-blue-600');
t.classList.add('border-transparent', 'text-gray-500');
});
tab.classList.remove('border-transparent', 'text-gray-500');
tab.classList.add('border-blue-500', 'text-blue-600');
// Show/hide sections
Object.keys(sections).forEach(key => {
if (key === targetSection) {
sections[key].classList.remove('hidden');
// Load data for specific sections
if (key === 'validation') {
loadValidationArticles();
} else if (key === 'published') {
loadPublishedPosts();
}
} else {
sections[key].classList.add('hidden');
}
});
});
});
}
// Initialize validation section
function initValidation() {
console.log('[Validation] Initializing validation section');
const refreshBtn = document.getElementById('refresh-validation-btn');
if (refreshBtn) {
refreshBtn.addEventListener('click', loadValidationArticles);
}
const refreshPublishedBtn = document.getElementById('refresh-published-btn');
if (refreshPublishedBtn) {
refreshPublishedBtn.addEventListener('click', loadPublishedPosts);
}
// Auto-load validation articles
const validationList = document.getElementById('validation-list');
if (validationList) {
console.log('[Validation] Loading validation articles...');
loadValidationArticles();
} else {
console.warn('[Validation] validation-list element not found');
}
// Initialize section navigation
initSectionNavigation();
}
// Run on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initValidation);
} else {
initValidation();
}
// ===========================================
// MANAGE SUBMISSION MODAL
// ===========================================
// Global state for submission modal
let currentSubmissionArticleId = null;
let currentSubmissionId = null;
let currentArticleData = null;
/**
* Initialize Manage Submission button handlers
*/
function initManageSubmissionHandlers() {
// Event delegation for manage submission buttons
document.addEventListener('click', (e) => {
if (e.target.classList.contains('manage-submission')) {
const articleId = e.target.dataset.articleId;
const submissionId = e.target.dataset.submissionId;
openManageSubmissionModal(articleId, submissionId || null);
}
});
}
// Call this when page loads
initManageSubmissionHandlers();