// Auth check const token = localStorage.getItem('admin_token'); const user = JSON.parse(localStorage.getItem('admin_user') || '{}'); if (!token) { window.location.href = '/admin/login.html'; } // Display admin name document.getElementById('admin-name').textContent = user.email || 'Admin'; // Logout document.getElementById('logout-btn').addEventListener('click', () => { localStorage.removeItem('admin_token'); localStorage.removeItem('admin_user'); window.location.href = '/admin/login.html'; }); // Navigation const navLinks = document.querySelectorAll('.nav-link'); const sections = { 'overview': document.getElementById('overview-section'), 'moderation': document.getElementById('moderation-section'), 'users': document.getElementById('users-section'), 'documents': document.getElementById('documents-section') }; navLinks.forEach(link => { link.addEventListener('click', (e) => { const href = link.getAttribute('href'); // Only handle hash-based navigation (internal sections) // Let full URLs navigate normally if (!href || !href.startsWith('#')) { return; // Allow default navigation } e.preventDefault(); const section = href.substring(1); // Update active link navLinks.forEach(l => l.classList.remove('active', 'bg-blue-100', 'text-blue-700')); link.classList.add('active', 'bg-blue-100', 'text-blue-700'); // Show section Object.values(sections).forEach(s => s.classList.add('hidden')); if (sections[section]) { sections[section].classList.remove('hidden'); loadSection(section); } }); }); // API helper async function apiRequest(endpoint, options = {}) { const response = await fetch(endpoint, { ...options, headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', ...options.headers } }); if (response.status === 401) { localStorage.removeItem('admin_token'); window.location.href = '/admin/login.html'; return; } return response.json(); } // Load statistics async function loadStatistics() { try { const response = await apiRequest('/api/admin/stats'); if (!response.success || !response.stats) { console.error('Invalid stats response:', response); return; } const stats = response.stats; document.getElementById('stat-documents').textContent = stats.documents?.total || 0; document.getElementById('stat-pending').textContent = stats.moderation?.total_pending || 0; document.getElementById('stat-approved').textContent = stats.blog?.published || 0; document.getElementById('stat-users').textContent = stats.users?.total || 0; } catch (error) { console.error('Failed to load statistics:', error); } } // Load sync health status async function loadSyncHealth() { const statusEl = document.getElementById('sync-status'); const badgeEl = document.getElementById('sync-badge'); const detailsEl = document.getElementById('sync-details'); const iconContainerEl = document.getElementById('sync-icon-container'); try { const response = await apiRequest('/api/admin/sync/health'); if (!response.success || !response.health) { console.error('Invalid sync health response:', response); statusEl.textContent = 'Error'; badgeEl.textContent = 'Error'; badgeEl.className = 'px-2 py-1 text-xs rounded-full bg-red-100 text-red-800'; detailsEl.textContent = 'Failed to check sync health'; iconContainerEl.className = 'flex-shrink-0 bg-red-100 rounded-md p-3'; return; } const health = response.health; const counts = health.counts; // Update status text statusEl.textContent = `File: ${counts.file} | DB: ${counts.database}`; // Update badge and icon based on severity if (health.severity === 'success') { badgeEl.textContent = '✓ Synced'; badgeEl.className = 'px-2 py-1 text-xs rounded-full bg-green-100 text-green-800'; iconContainerEl.className = 'flex-shrink-0 bg-green-100 rounded-md p-3'; iconContainerEl.querySelector('svg').classList.remove('text-gray-600', 'text-yellow-600', 'text-red-600'); iconContainerEl.querySelector('svg').classList.add('text-green-600'); } else if (health.severity === 'warning') { badgeEl.textContent = '⚠ Desync'; badgeEl.className = 'px-2 py-1 text-xs rounded-full bg-yellow-100 text-yellow-800'; iconContainerEl.className = 'flex-shrink-0 bg-yellow-100 rounded-md p-3'; iconContainerEl.querySelector('svg').classList.remove('text-gray-600', 'text-green-600', 'text-red-600'); iconContainerEl.querySelector('svg').classList.add('text-yellow-600'); } else { badgeEl.textContent = '✗ Critical'; badgeEl.className = 'px-2 py-1 text-xs rounded-full bg-red-100 text-red-800'; iconContainerEl.className = 'flex-shrink-0 bg-red-100 rounded-md p-3'; iconContainerEl.querySelector('svg').classList.remove('text-gray-600', 'text-green-600', 'text-yellow-600'); iconContainerEl.querySelector('svg').classList.add('text-red-600'); } // Update details if (counts.difference === 0) { detailsEl.textContent = health.message; } else { const missing = health.details?.missingInDatabase?.length || 0; const orphaned = health.details?.orphanedInDatabase?.length || 0; detailsEl.textContent = `${health.message} (Missing: ${missing}, Orphaned: ${orphaned})`; } } catch (error) { console.error('Failed to load sync health:', error); statusEl.textContent = 'Error'; badgeEl.textContent = 'Error'; badgeEl.className = 'px-2 py-1 text-xs rounded-full bg-red-100 text-red-800'; detailsEl.textContent = 'Failed to check sync health'; iconContainerEl.className = 'flex-shrink-0 bg-red-100 rounded-md p-3'; } } // Trigger manual sync async function triggerSync() { const button = document.getElementById('sync-trigger-btn'); const originalText = button.textContent; try { // Disable button and show loading state button.disabled = true; button.textContent = 'Syncing...'; const response = await apiRequest('/api/admin/sync/trigger', { method: 'POST' }); if (response.success) { // Show success message button.textContent = '✓ Synced'; button.classList.remove('bg-blue-600', 'hover:bg-blue-700'); button.classList.add('bg-green-600'); // Reload health status and stats await loadSyncHealth(); await loadStatistics(); // Reset button after 2 seconds setTimeout(() => { button.textContent = originalText; button.classList.remove('bg-green-600'); button.classList.add('bg-blue-600', 'hover:bg-blue-700'); button.disabled = false; }, 2000); } else { throw new Error(response.message || 'Sync failed'); } } catch (error) { console.error('Manual sync error:', error); button.textContent = '✗ Failed'; button.classList.remove('bg-blue-600', 'hover:bg-blue-700'); button.classList.add('bg-red-600'); // Reset button after 2 seconds setTimeout(() => { button.textContent = originalText; button.classList.remove('bg-red-600'); button.classList.add('bg-blue-600', 'hover:bg-blue-700'); button.disabled = false; }, 2000); } } // Load recent activity async function loadRecentActivity() { const container = document.getElementById('recent-activity'); try { const response = await apiRequest('/api/admin/activity'); if (!response.success || !response.activity || response.activity.length === 0) { container.innerHTML = '
No recent activity
'; return; } container.innerHTML = response.activity.map(item => { // Generate description from activity data const action = item.action || 'reviewed'; const itemType = item.item_type || 'item'; const description = `${action.charAt(0).toUpperCase() + action.slice(1)} ${itemType}`; return `
${getActivityIcon(action)}

${description}

${formatDate(item.timestamp)}

`; }).join(''); } catch (error) { console.error('Failed to load activity:', error); container.innerHTML = '
Failed to load activity
'; } } // Load moderation queue async function loadModerationQueue(filter = 'all') { const container = document.getElementById('moderation-queue'); try { const response = await apiRequest(`/api/admin/moderation?type=${filter}`); if (!response.success || !response.items || response.items.length === 0) { container.innerHTML = '
No items pending review
'; return; } container.innerHTML = response.items.map(item => `
${item.type} ${formatDate(item.submitted_at)}

${item.title}

${truncate(item.content || item.description, 150)}

`).join(''); } catch (error) { console.error('Failed to load moderation queue:', error); container.innerHTML = '
Failed to load queue
'; } } // Load users async function loadUsers() { const container = document.getElementById('users-list'); try { const response = await apiRequest('/api/admin/users'); if (!response.success || !response.users || response.users.length === 0) { container.innerHTML = '
No users found
'; return; } container.innerHTML = response.users.map(user => `
${user.email.charAt(0).toUpperCase()}

${user.email}

Role: ${user.role}

${user.role} ${user._id !== user._id ? ` ` : ''}
`).join(''); } catch (error) { console.error('Failed to load users:', error); container.innerHTML = '
Failed to load users
'; } } // Load documents async function loadDocuments() { const container = document.getElementById('documents-list'); try { const response = await apiRequest('/api/documents'); if (!response.success || !response.documents || response.documents.length === 0) { container.innerHTML = '
No documents found
'; return; } container.innerHTML = response.documents.map(doc => { const visibilityBadge = getVisibilityBadge(doc.visibility || 'internal'); const statusBadge = getStatusBadge(doc.workflow_status || 'draft'); const canPublish = doc.visibility === 'internal' && doc.workflow_status !== 'published'; const canUnpublish = doc.visibility === 'public' && doc.workflow_status === 'published'; return `

${doc.title}

${doc.quadrant || 'No quadrant'}

${statusBadge} ${visibilityBadge} ${doc.category ? `Category: ${doc.category}` : ''}
View ${canPublish ? ` ` : ''} ${canUnpublish ? ` ` : ''}
`; }).join(''); } catch (error) { console.error('Failed to load documents:', error); container.innerHTML = '
Failed to load documents
'; } } // Load section data function loadSection(section) { switch (section) { case 'overview': loadStatistics(); loadRecentActivity(); break; case 'moderation': loadModerationQueue(); break; case 'users': loadUsers(); break; case 'documents': loadDocuments(); break; } } // Approve item async function approveItem(itemId) { if (!confirm('Approve this item?')) return; try { const response = await apiRequest(`/api/admin/moderation/${itemId}/approve`, { method: 'POST' }); if (response.success) { loadModerationQueue(); loadStatistics(); } else { alert('Failed to approve item'); } } catch (error) { console.error('Approval error:', error); alert('Failed to approve item'); } } // Reject item async function rejectItem(itemId) { if (!confirm('Reject this item?')) return; try { const response = await apiRequest(`/api/admin/moderation/${itemId}/reject`, { method: 'POST' }); if (response.success) { loadModerationQueue(); loadStatistics(); } else { alert('Failed to reject item'); } } catch (error) { console.error('Rejection error:', error); alert('Failed to reject item'); } } // Delete user async function deleteUser(userId) { if (!confirm('Delete this user? This action cannot be undone.')) return; try { const response = await apiRequest(`/api/admin/users/${userId}`, { method: 'DELETE' }); if (response.success) { loadUsers(); loadStatistics(); } else { alert(response.message || 'Failed to delete user'); } } catch (error) { console.error('Delete error:', error); alert('Failed to delete user'); } } // Delete document async function deleteDocument(docId) { if (!confirm('Delete this document? This action cannot be undone.')) return; try { const response = await apiRequest(`/api/documents/${docId}`, { method: 'DELETE' }); if (response.success) { loadDocuments(); loadStatistics(); } else { alert('Failed to delete document'); } } catch (error) { console.error('Delete error:', error); alert('Failed to delete document'); } } // Open publish modal async function openPublishModal(docId) { try { const response = await apiRequest(`/api/documents/${docId}`); if (!response.success || !response.document) { alert('Failed to load document'); return; } const doc = response.document; const modalHTML = `

Publish Document

Title: ${doc.title}

Current Status: ${doc.workflow_status || 'draft'}

Higher numbers appear first

`; document.body.insertAdjacentHTML('beforeend', modalHTML); // Store doc ID for later document.getElementById('publish-form').dataset.docId = docId; // Handle form submission document.getElementById('publish-form').addEventListener('submit', async (e) => { e.preventDefault(); const category = document.getElementById('publish-category').value; const order = parseInt(document.getElementById('publish-order').value) || 0; await publishDocument(docId, category, order); }); } catch (error) { console.error('Failed to open publish modal:', error); alert('Failed to open publish modal'); } } // Publish document async function publishDocument(docId, category, order) { try { const response = await apiRequest(`/api/documents/${docId}/publish`, { method: 'POST', body: JSON.stringify({ category, order }) }); if (response.success) { closePublishModal(); loadDocuments(); loadStatistics(); alert('Document published successfully'); } else { alert(response.message || 'Failed to publish document'); } } catch (error) { console.error('Publish error:', error); alert('Failed to publish document'); } } // Close publish modal function closePublishModal() { document.getElementById('publish-modal')?.remove(); } // Open unpublish modal async function openUnpublishModal(docId) { try { const response = await apiRequest(`/api/documents/${docId}`); if (!response.success || !response.document) { alert('Failed to load document'); return; } const doc = response.document; const modalHTML = `

Unpublish Document

Title: ${doc.title}

Current Visibility: ${doc.visibility}

Category: ${doc.category || 'None'}

This will be recorded in the audit trail

`; document.body.insertAdjacentHTML('beforeend', modalHTML); // Store doc ID for later document.getElementById('unpublish-form').dataset.docId = docId; // Handle form submission document.getElementById('unpublish-form').addEventListener('submit', async (e) => { e.preventDefault(); const reason = document.getElementById('unpublish-reason').value; await unpublishDocument(docId, reason); }); } catch (error) { console.error('Failed to open unpublish modal:', error); alert('Failed to open unpublish modal'); } } // Unpublish document async function unpublishDocument(docId, reason) { try { const response = await apiRequest(`/api/documents/${docId}/unpublish`, { method: 'POST', body: JSON.stringify({ reason }) }); if (response.success) { closeUnpublishModal(); loadDocuments(); loadStatistics(); alert('Document unpublished successfully'); } else { alert(response.message || 'Failed to unpublish document'); } } catch (error) { console.error('Unpublish error:', error); alert('Failed to unpublish document'); } } // Close unpublish modal function closeUnpublishModal() { document.getElementById('unpublish-modal')?.remove(); } // Utility functions function getVisibilityBadge(visibility) { const badges = { 'public': 'Public', 'internal': 'Internal', 'confidential': 'Confidential', 'archived': 'Archived' }; return badges[visibility] || badges['internal']; } function getStatusBadge(status) { const badges = { 'draft': 'Draft', 'review': 'Review', 'published': 'Published' }; return badges[status] || badges['draft']; } function getActivityColor(type) { const colors = { 'create': 'bg-green-500', 'update': 'bg-blue-500', 'delete': 'bg-red-500', 'approve': 'bg-purple-500' }; return colors[type] || 'bg-gray-500'; } function getActivityIcon(type) { const icons = { 'create': '+', 'update': '↻', 'delete': '×', 'approve': '✓' }; return icons[type] || '•'; } function formatDate(dateString) { if (!dateString) return 'Unknown'; const date = new Date(dateString); return date.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } function truncate(str, length) { if (!str) return ''; return str.length > length ? str.substring(0, length) + '...' : str; } // Queue filter document.getElementById('queue-filter')?.addEventListener('change', (e) => { loadModerationQueue(e.target.value); }); // Initialize loadStatistics(); loadRecentActivity(); loadSyncHealth(); // Auto-refresh sync health every 60 seconds setInterval(() => { loadSyncHealth(); }, 60000); // Event delegation for data-action buttons (CSP compliance) document.addEventListener('click', (e) => { const button = e.target.closest('[data-action]'); if (!button) return; const action = button.dataset.action; const arg0 = button.dataset.arg0; switch (action) { case 'approveItem': approveItem(arg0); break; case 'rejectItem': rejectItem(arg0); break; case 'deleteUser': deleteUser(arg0); break; case 'deleteDocument': deleteDocument(arg0); break; case 'openPublishModal': openPublishModal(arg0); break; case 'openUnpublishModal': openUnpublishModal(arg0); break; case 'closePublishModal': closePublishModal(); break; case 'closeUnpublishModal': closeUnpublishModal(); break; case 'triggerSync': triggerSync(); break; } });