listmonk sync

This commit is contained in:
2025-08-15 11:14:38 -06:00
parent e36610ceed
commit 6ac45f7adf
20 changed files with 3416 additions and 6 deletions

View File

@@ -56,6 +56,7 @@ document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
loadWalkSheetConfig();
initializeNocodbLinks();
initializeListmonkLinks();
}, 100);
// Check if URL has a hash to show specific section
@@ -1998,6 +1999,88 @@ function setAdminNocodbLink(elementId, url) {
}
}
// Initialize Listmonk links in admin panel
async function initializeListmonkLinks() {
console.log('Starting Listmonk links initialization...');
try {
// Since we're in the admin panel, the user is already verified as admin
// by the requireAdmin middleware. Let's get the URLs from the server directly.
console.log('Fetching Listmonk URLs for admin panel...');
const configResponse = await fetch('/api/admin/listmonk-urls');
if (!configResponse.ok) {
throw new Error(`Listmonk URLs fetch failed: ${configResponse.status} ${configResponse.statusText}`);
}
const config = await configResponse.json();
console.log('Listmonk URLs received:', config);
if (config.success && config.listmonkUrls) {
console.log('Setting up Listmonk links with URLs:', config.listmonkUrls);
// Set up admin dashboard Listmonk links
setAdminListmonkLink('admin-listmonk-admin-link', config.listmonkUrls.adminUrl);
setAdminListmonkLink('admin-listmonk-lists-link', config.listmonkUrls.listsUrl);
setAdminListmonkLink('admin-listmonk-campaigns-link', config.listmonkUrls.campaignsUrl);
setAdminListmonkLink('admin-listmonk-subscribers-link', config.listmonkUrls.subscribersUrl);
setAdminListmonkLink('admin-listmonk-settings-link', config.listmonkUrls.settingsUrl);
console.log('Listmonk links initialized in admin panel');
} else {
console.warn('No Listmonk URLs found in admin config response');
// Hide the Listmonk section if no URLs are available
const listmonkSection = document.getElementById('listmonk-links');
const listmonkNav = document.querySelector('.admin-nav a[href="#listmonk-links"]');
if (listmonkSection) {
listmonkSection.style.display = 'none';
console.log('Hidden Listmonk section');
}
if (listmonkNav) {
listmonkNav.style.display = 'none';
console.log('Hidden Listmonk nav link');
}
}
} catch (error) {
console.error('Error initializing Listmonk links in admin panel:', error);
// Hide the Listmonk section on error
const listmonkSection = document.getElementById('listmonk-links');
const listmonkNav = document.querySelector('.admin-nav a[href="#listmonk-links"]');
if (listmonkSection) {
listmonkSection.style.display = 'none';
console.log('Hidden Listmonk section due to error');
}
if (listmonkNav) {
listmonkNav.style.display = 'none';
console.log('Hidden Listmonk nav link due to error');
}
}
}
// Helper function to set admin Listmonk link href
function setAdminListmonkLink(elementId, url) {
console.log(`Setting up Listmonk link: ${elementId} = ${url}`);
const element = document.getElementById(elementId);
if (element && url) {
element.href = url;
element.style.display = 'inline-flex';
// Remove any disabled state
element.classList.remove('btn-disabled');
element.removeAttribute('disabled');
console.log(` Successfully set up ${elementId}`);
} else if (element) {
element.style.display = 'none';
// Add disabled state if no URL
element.classList.add('btn-disabled');
element.setAttribute('disabled', 'disabled');
element.href = '#';
console.log(` Disabled ${elementId} - no URL provided`);
} else {
console.error(` Element not found: ${elementId}`);
}
}
// Shift User Management Functions
let currentShiftData = null;
let allUsers = [];

View File

@@ -0,0 +1,570 @@
/**
* Admin Listmonk Management Functions
* Handles admin interface for email list synchronization
*/
// Global variables for admin Listmonk functionality
let syncInProgress = false;
let syncProgressInterval = null;
/**
* Initialize Listmonk admin section
*/
async function initListmonkAdmin() {
await refreshListmonkStatus();
await loadListmonkStats();
}
/**
* Refresh the Listmonk sync status display
*/
async function refreshListmonkStatus() {
console.log('🔄 Refreshing Listmonk status...');
try {
const response = await fetch('/api/listmonk/status', {
credentials: 'include'
});
console.log('📡 Status response:', response.status, response.statusText);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const status = await response.json();
console.log('📊 Status data:', status);
updateStatusDisplay(status);
// Update global status if available
if (window.listmonkStatus) {
window.listmonkStatus.currentStatus = status;
}
} catch (error) {
console.error('Failed to refresh Listmonk status:', error);
updateStatusDisplay({
enabled: false,
connected: false,
lastError: `Status check failed: ${error.message}`
});
}
}
/**
* Update the status display in the admin panel
*/
function updateStatusDisplay(status) {
console.log('🎨 Updating status display with:', status);
const connectionStatus = document.getElementById('connection-status');
const autosyncStatus = document.getElementById('autosync-status');
const lastError = document.getElementById('last-error');
console.log('🔍 Status elements found:', {
connectionStatus: !!connectionStatus,
autosyncStatus: !!autosyncStatus,
lastError: !!lastError
});
if (connectionStatus) {
if (status.enabled && status.connected) {
connectionStatus.innerHTML = '<span class="status-connected">✅ Connected</span>';
} else if (status.enabled && !status.connected) {
connectionStatus.innerHTML = '<span class="status-error">❌ Failed</span>';
} else {
connectionStatus.innerHTML = '<span class="status-disabled">⭕ Disabled</span>';
}
console.log('✅ Connection status updated:', connectionStatus.innerHTML);
}
if (autosyncStatus) {
if (status.enabled) {
autosyncStatus.innerHTML = '<span class="status-connected">✅ Enabled</span>';
} else {
autosyncStatus.innerHTML = '<span class="status-disabled">⭕ Disabled</span>';
}
console.log('✅ Auto-sync status updated:', autosyncStatus.innerHTML);
}
if (lastError) {
if (status.lastError) {
lastError.innerHTML = `<span class="status-error">${escapeHtml(status.lastError)}</span>`;
} else {
lastError.innerHTML = '<span class="status-connected">None</span>';
}
console.log('✅ Last error updated:', lastError.innerHTML);
}
}
/**
* Load and display Listmonk list statistics
*/
async function loadListmonkStats() {
try {
const response = await fetch('/api/listmonk/stats', {
credentials: 'include'
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
console.log('📊 Stats API response:', data); // Debug log
if (data.success && data.stats) {
// Ensure stats is an array
const statsArray = Array.isArray(data.stats) ? data.stats : [];
console.log('📊 Stats array:', statsArray); // Debug log
displayListStats(statsArray);
} else {
console.warn('No Listmonk stats available:', data.error || 'Unknown error');
displayListStats([]);
}
} catch (error) {
console.error('Failed to load Listmonk stats:', error);
}
}
/**
* Display list statistics in the admin panel
*/
function displayListStats(stats) {
const statusContent = document.getElementById('sync-status-display');
if (!statusContent) return;
console.log('📊 displayListStats called with:', stats, 'Type:', typeof stats); // Debug log
// Ensure stats is an array
const statsArray = Array.isArray(stats) ? stats : [];
console.log('📊 Stats array after conversion:', statsArray, 'Length:', statsArray.length); // Debug log
// Add stats section if it doesn't exist
let statsSection = document.getElementById('listmonk-stats-section');
if (!statsSection) {
statsSection = document.createElement('div');
statsSection.id = 'listmonk-stats-section';
statsSection.innerHTML = '<h4>Email Lists</h4>';
statusContent.appendChild(statsSection);
}
// Clear existing stats
const existingStats = statsSection.querySelector('.stats-list');
if (existingStats) {
existingStats.remove();
}
// Create stats display
const statsList = document.createElement('div');
statsList.className = 'stats-list';
if (statsArray.length === 0) {
statsList.innerHTML = '<p class="status-disabled">No email lists found</p>';
} else {
statsArray.forEach(list => {
const statRow = document.createElement('div');
statRow.className = 'status-row';
statRow.innerHTML = `
<span title="${escapeHtml(list.description || '')}">${escapeHtml(list.name)}</span>
<span class="status-connected">${list.subscriberCount} subscribers</span>
`;
statsList.appendChild(statRow);
});
}
statsSection.appendChild(statsList);
}
/**
* Sync data to Listmonk
* @param {string} type - 'locations', 'users', or 'all'
*/
async function syncToListmonk(type) {
if (syncInProgress) {
showNotification('Sync already in progress', 'warning');
return;
}
syncInProgress = true;
const progressSection = document.getElementById('sync-progress');
const resultsDiv = document.getElementById('sync-results');
const progressBar = document.getElementById('sync-progress-bar');
// Show progress section
if (progressSection) {
progressSection.style.display = 'block';
}
// Reset progress
if (progressBar) {
progressBar.style.width = '0%';
}
if (resultsDiv) {
resultsDiv.innerHTML = '<div class="sync-result info">Starting sync...</div>';
}
// Disable sync buttons
const buttons = document.querySelectorAll('.sync-buttons .btn');
buttons.forEach(btn => {
btn.disabled = true;
btn.style.opacity = '0.6';
});
try {
const response = await fetch(`/api/listmonk/sync/${type}`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
// Animate progress to 100%
if (progressBar) {
progressBar.style.width = '100%';
}
// Display results
displaySyncResults(result, resultsDiv);
// Refresh stats after successful sync
setTimeout(() => {
loadListmonkStats();
}, 1000);
} catch (error) {
console.error('Sync failed:', error);
if (resultsDiv) {
resultsDiv.innerHTML = `
<div class="sync-result error">
<strong>Sync Failed:</strong> ${escapeHtml(error.message)}
</div>
`;
}
showNotification('Sync failed: ' + error.message, 'error');
} finally {
syncInProgress = false;
// Re-enable sync buttons
buttons.forEach(btn => {
btn.disabled = false;
btn.style.opacity = '1';
});
}
}
/**
* Display sync results in the admin panel
*/
function displaySyncResults(result, resultsDiv) {
if (!resultsDiv) return;
let html = '';
if (result.success) {
html += `<div class="sync-result success">
<strong>✅ ${escapeHtml(result.message)}</strong>
</div>`;
// Show detailed results for different sync types
if (result.results) {
if (result.results.locations) {
html += formatSyncResults('Locations', result.results.locations);
}
if (result.results.users) {
html += formatSyncResults('Users', result.results.users);
}
// For single type syncs
if (result.results.total !== undefined) {
html += formatSyncResults('Items', result.results);
}
}
showNotification('Sync completed successfully!', 'success');
} else {
html += `<div class="sync-result error">
<strong>❌ Sync Failed:</strong> ${escapeHtml(result.error)}
</div>`;
showNotification('Sync failed', 'error');
}
resultsDiv.innerHTML = html;
}
/**
* Format sync results for display
*/
function formatSyncResults(type, results) {
let html = `<div class="sync-result info">
<strong>${type}:</strong>
${results.success} succeeded, ${results.failed} failed
(${results.total} total)
</div>`;
// Show errors if any
if (results.errors && results.errors.length > 0) {
html += `<div class="sync-result error">
<strong>Errors:</strong>
<ul style="margin: 5px 0 0 20px; font-size: 12px;">`;
// Show only first 5 errors to avoid overwhelming the UI
const errorsToShow = results.errors.slice(0, 5);
errorsToShow.forEach(error => {
html += `<li>${escapeHtml(error.email || error.id)}: ${escapeHtml(error.error)}</li>`;
});
if (results.errors.length > 5) {
html += `<li><em>... and ${results.errors.length - 5} more errors</em></li>`;
}
html += `</ul></div>`;
}
return html;
}
/**
* Test Listmonk connection
*/
async function testListmonkConnection() {
try {
const response = await fetch('/api/listmonk/test-connection', {
credentials: 'include'
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = await response.json();
if (result.success && result.connected) {
showNotification('✅ Listmonk connection successful!', 'success');
} else {
showNotification('❌ Connection failed: ' + (result.message || 'Unknown error'), 'error');
}
// Refresh status after test
setTimeout(refreshListmonkStatus, 1000);
} catch (error) {
console.error('Connection test failed:', error);
showNotification('❌ Connection test failed: ' + error.message, 'error');
}
}
/**
* Reinitialize Listmonk lists
*/
async function reinitializeListmonk() {
if (!confirm('This will recreate all email lists. Are you sure?')) {
return;
}
try {
const response = await fetch('/api/listmonk/reinitialize', {
method: 'POST',
credentials: 'include'
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = await response.json();
if (result.success) {
showNotification('✅ Lists reinitialized successfully!', 'success');
setTimeout(() => {
refreshListmonkStatus();
loadListmonkStats();
}, 1000);
} else {
showNotification('❌ Failed to reinitialize: ' + result.message, 'error');
}
} catch (error) {
console.error('Reinitialize failed:', error);
showNotification('❌ Reinitialize failed: ' + error.message, 'error');
}
}
/**
* Utility function to escape HTML
*/
function escapeHtml(text) {
if (typeof text !== 'string') return text;
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Fallback notification function if showNotification is not available
*/
function showNotification(message, type = 'info') {
// Check if a global showNotification function exists
if (typeof window.showNotification === 'function' && window.showNotification !== showNotification) {
window.showNotification(message, type);
} else {
// Simple fallback - log to console and show alert for errors
console.log(`[${type.toUpperCase()}] ${message}`);
if (type === 'error') {
console.error(message);
alert(`ERROR: ${message}`);
} else if (type === 'warning') {
console.warn(message);
alert(`WARNING: ${message}`);
} else if (type === 'success') {
console.info(`SUCCESS: ${message}`);
}
}
}
// Auto-initialize when admin page loads
function initializeListmonkEventListeners() {
console.log('🔧 Initializing Listmonk event listeners...');
// Sync Locations button
const syncLocationsBtn = document.getElementById('sync-locations-btn');
if (syncLocationsBtn) {
syncLocationsBtn.addEventListener('click', () => {
console.log('📍 Sync Locations clicked');
syncToListmonk('locations');
});
console.log('✅ Sync Locations button listener added');
} else {
console.warn('❌ Sync Locations button not found');
}
// Sync Users button
const syncUsersBtn = document.getElementById('sync-users-btn');
if (syncUsersBtn) {
syncUsersBtn.addEventListener('click', () => {
console.log('👤 Sync Users clicked');
syncToListmonk('users');
});
console.log('✅ Sync Users button listener added');
} else {
console.warn('❌ Sync Users button not found');
}
// Sync All button
const syncAllBtn = document.getElementById('sync-all-btn');
if (syncAllBtn) {
syncAllBtn.addEventListener('click', () => {
console.log('🔄 Sync All clicked');
syncToListmonk('all');
});
console.log('✅ Sync All button listener added');
} else {
console.warn('❌ Sync All button not found');
}
// Refresh Status button
const refreshStatusBtn = document.getElementById('refresh-status-btn');
if (refreshStatusBtn) {
refreshStatusBtn.addEventListener('click', () => {
console.log('🔍 Refresh Status clicked');
refreshListmonkStatus();
});
console.log('✅ Refresh Status button listener added');
} else {
console.warn('❌ Refresh Status button not found');
}
// Test Connection button
const testConnectionBtn = document.getElementById('test-connection-btn');
if (testConnectionBtn) {
testConnectionBtn.addEventListener('click', () => {
console.log('🔗 Test Connection clicked');
testListmonkConnection();
});
console.log('✅ Test Connection button listener added');
} else {
console.warn('❌ Test Connection button not found');
}
// Reinitialize Lists button
const reinitializeListsBtn = document.getElementById('reinitialize-lists-btn');
if (reinitializeListsBtn) {
reinitializeListsBtn.addEventListener('click', () => {
console.log('🛠️ Reinitialize Lists clicked');
reinitializeListmonk();
});
console.log('✅ Reinitialize Lists button listener added');
} else {
console.warn('❌ Reinitialize Lists button not found');
}
}
function attemptInitialization(attempt = 1) {
console.log(`🔄 Attempting Listmonk initialization (attempt ${attempt})`);
// Try to initialize admin and event listeners directly
try {
initListmonkAdmin();
initializeListmonkEventListeners();
console.log('✅ Listmonk initialization completed successfully');
return true;
} catch (error) {
console.warn(`⚠️ Attempt ${attempt} failed:`, error.message);
if (attempt < 5) {
console.log(`⏱️ Retrying in ${attempt * 500}ms...`);
setTimeout(() => attemptInitialization(attempt + 1), attempt * 500);
return false;
} else {
console.error('❌ All initialization attempts failed');
// Still try to initialize event listeners as a fallback
try {
initializeListmonkEventListeners();
console.log('✅ Event listeners initialized as fallback');
} catch (fallbackError) {
console.error('❌ Even fallback initialization failed:', fallbackError);
}
return false;
}
}
}
// Also try a more direct approach - just initialize when the page is ready
function directInitialization() {
console.log('🚀 Direct Listmonk initialization...');
try {
initListmonkAdmin();
initializeListmonkEventListeners();
console.log('✅ Direct initialization successful');
} catch (error) {
console.error('Direct initialization failed:', error);
// Try just the event listeners
try {
initializeListmonkEventListeners();
console.log('✅ Event listeners initialized directly');
} catch (listenerError) {
console.error('❌ Even direct event listener initialization failed:', listenerError);
}
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
attemptInitialization();
// Also try direct initialization after a delay
setTimeout(directInitialization, 2000);
});
} else {
attemptInitialization();
// Also try direct initialization after a delay
setTimeout(directInitialization, 1000);
}
// Export functions for global use
window.syncToListmonk = syncToListmonk;
window.refreshListmonkStatus = refreshListmonkStatus;
window.testListmonkConnection = testListmonkConnection;
window.reinitializeListmonk = reinitializeListmonk;

View File

@@ -0,0 +1,225 @@
/**
* Listmonk Status Manager
* Handles real-time status monitoring and user notifications for email list sync
*/
class ListmonkStatus {
constructor() {
this.statusElement = null;
this.checkInterval = null;
this.lastErrorShown = null;
this.currentStatus = {
enabled: false,
connected: false,
lastError: null
};
this.init();
}
init() {
// Only initialize if user is authenticated
if (window.currentUser) {
this.createStatusIndicator();
this.startStatusCheck();
}
}
createStatusIndicator() {
// Create status indicator element
const indicator = document.createElement('div');
indicator.id = 'listmonk-status';
indicator.className = 'listmonk-status-indicator checking';
indicator.innerHTML = `
<span class="status-icon">⏳</span>
<span class="status-text">Email Sync</span>
`;
// Find a good place to put it - try header actions first
let container = document.querySelector('.header-actions');
if (!container) {
// Fallback to header nav
container = document.querySelector('.header-nav');
}
if (!container) {
// Fallback to header itself
container = document.querySelector('header');
}
if (container) {
container.appendChild(indicator);
this.statusElement = indicator;
}
}
async checkStatus() {
try {
const response = await fetch('/api/listmonk/status', {
credentials: 'include'
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const status = await response.json();
this.updateIndicator(status);
} catch (error) {
console.error('Failed to check Listmonk status:', error);
this.updateIndicator({
enabled: false,
connected: false,
lastError: error.message
});
}
}
updateIndicator(status) {
if (!this.statusElement) return;
const icon = this.statusElement.querySelector('.status-icon');
const text = this.statusElement.querySelector('.status-text');
// Update current status
const wasConnected = this.currentStatus.connected;
this.currentStatus = status;
if (!status.enabled) {
this.statusElement.className = 'listmonk-status-indicator disabled';
icon.innerHTML = '⭕';
text.textContent = 'Sync Off';
this.statusElement.title = 'Email list synchronization is disabled';
} else if (status.connected) {
this.statusElement.className = 'listmonk-status-indicator connected';
icon.innerHTML = '✅';
text.textContent = 'Sync On';
this.statusElement.title = 'Email lists are synchronizing automatically';
// If we just reconnected, show a brief success message
if (!wasConnected && this.lastErrorShown) {
this.showReconnectedNotification();
}
} else {
this.statusElement.className = 'listmonk-status-indicator error';
icon.innerHTML = '❌';
text.textContent = 'Sync Error';
this.statusElement.title = status.lastError || 'Email list sync failed - check configuration';
// Show popup warning if this is a new error or first time seeing this error
const errorKey = status.lastError + (status.lastErrorTime || '');
if ((!this.lastErrorShown || this.lastErrorShown !== errorKey) && status.lastError) {
this.showSyncError(status.lastError);
this.lastErrorShown = errorKey;
}
}
}
showSyncError(error) {
// Don't spam notifications
if (document.querySelector('.sync-error-notification')) {
return;
}
// Create and show error notification
const notification = document.createElement('div');
notification.className = 'sync-error-notification';
notification.innerHTML = `
<div class="notification-header">
<strong>⚠️ Email Sync Error</strong>
<button class="close-btn" onclick="this.parentElement.parentElement.remove()">×</button>
</div>
<div class="notification-body">
<p>The email list synchronization is not working:</p>
<code>${this.escapeHtml(error || 'Connection failed')}</code>
<p>New contacts will be saved locally but won't sync to email lists until this is resolved.</p>
<p>Please contact your administrator if this persists.</p>
</div>
`;
document.body.appendChild(notification);
// Auto-remove after 15 seconds
setTimeout(() => {
if (notification && notification.parentNode) {
notification.remove();
}
}, 15000);
}
showReconnectedNotification() {
// Don't show if there's already a notification
if (document.querySelector('.sync-success-notification')) {
return;
}
const notification = document.createElement('div');
notification.className = 'sync-success-notification';
notification.innerHTML = `
<div class="notification-header">
<strong>✅ Email Sync Restored</strong>
<button class="close-btn" onclick="this.parentElement.parentElement.remove()">×</button>
</div>
<div class="notification-body">
<p>Email list synchronization is working again. New contacts will now sync automatically.</p>
</div>
`;
document.body.appendChild(notification);
// Auto-remove after 5 seconds
setTimeout(() => {
if (notification && notification.parentNode) {
notification.remove();
}
}, 5000);
}
startStatusCheck() {
// Check immediately
this.checkStatus();
// Then check every 30 seconds
this.checkInterval = setInterval(() => {
this.checkStatus();
}, 30000);
}
stopStatusCheck() {
if (this.checkInterval) {
clearInterval(this.checkInterval);
this.checkInterval = null;
}
}
// Utility method for HTML escaping
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Manual refresh method for admin panel
async refresh() {
if (this.statusElement) {
this.statusElement.className = 'listmonk-status-indicator checking';
this.statusElement.querySelector('.status-icon').innerHTML = '⏳';
}
await this.checkStatus();
}
// Get current status (for admin panel)
getCurrentStatus() {
return { ...this.currentStatus };
}
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
// Only create if we don't already have one
if (!window.listmonkStatus) {
window.listmonkStatus = new ListmonkStatus();
}
});
// Export for use in other modules
window.ListmonkStatus = ListmonkStatus;