// Auth check (navbar-admin.js also handles this, but keep for safety)
const token = localStorage.getItem('admin_token');
if (!token) {
window.location.href = '/admin/login.html';
}
// Navigation - handled by navbar-admin.js component now
// Section navigation for dashboard tabs
const sections = {
'overview': document.getElementById('overview-section'),
'moderation': document.getElementById('moderation-section'),
'users': document.getElementById('users-section'),
'documents': document.getElementById('documents-section')
};
// Handle hash navigation within dashboard
function showSection(sectionName) {
Object.values(sections).forEach(s => {
if (s) s.classList.add('hidden');
});
if (sections[sectionName]) {
sections[sectionName].classList.remove('hidden');
loadSection(sectionName);
}
}
window.addEventListener('hashchange', () => {
const hash = window.location.hash.slice(1) || 'overview';
showSection(hash);
});
// Show initial section on load
document.addEventListener('DOMContentLoaded', () => {
const initialSection = window.location.hash.slice(1) || 'overview';
showSection(initialSection);
});
// 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'}
`;
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'}
`;
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;
}
});