/** * ZitiNexus Router Enrollment UI JavaScript */ class EnrollmentUI { constructor() { this.enrollmentInProgress = false; this.progressSteps = [ 'INIT', 'REQUIREMENTS', 'INSTALL', 'DIRECTORIES', 'REGISTER', 'CONFIG', 'ENROLL', 'SERVICE', 'START', 'REPORT', 'COMPLETE' ]; this.currentStep = 0; this.init(); } init() { this.bindEvents(); this.loadSystemStatus(); // Auto-refresh system status every 30 seconds setInterval(() => { if (!this.enrollmentInProgress) { this.loadSystemStatus(); } }, 30000); } bindEvents() { // Enrollment form submission const enrollForm = document.getElementById('enrollmentForm'); if (enrollForm) { enrollForm.addEventListener('submit', (e) => { e.preventDefault(); this.startEnrollment(); }); } // Hash key validation const hashKeyInput = document.getElementById('hashKey'); if (hashKeyInput) { hashKeyInput.addEventListener('input', (e) => { this.validateHashKey(e.target); }); } // API endpoint validation const apiEndpointInput = document.getElementById('apiEndpoint'); if (apiEndpointInput) { apiEndpointInput.addEventListener('input', (e) => { this.validateApiEndpoint(e.target); }); } // Refresh system status button const refreshBtn = document.getElementById('refreshStatus'); if (refreshBtn) { refreshBtn.addEventListener('click', () => { this.loadSystemStatus(); }); } // Clear logs button const clearLogsBtn = document.getElementById('clearLogs'); if (clearLogsBtn) { clearLogsBtn.addEventListener('click', () => { this.clearLogs(); }); } // Cleanup button const cleanupBtn = document.getElementById('cleanupBtn'); if (cleanupBtn) { cleanupBtn.addEventListener('click', () => { this.showCleanupConfirmation(); }); } } validateHashKey(input) { const value = input.value.trim(); const isValid = /^[a-fA-F0-9]{32}$/.test(value); if (value.length === 0) { this.setInputState(input, 'neutral'); } else if (isValid) { this.setInputState(input, 'valid'); } else { this.setInputState(input, 'invalid', 'Hash key must be 32 hexadecimal characters'); } return isValid; } validateApiEndpoint(input) { const value = input.value.trim(); const isValid = /^https?:\/\/.+/.test(value); if (value.length === 0) { this.setInputState(input, 'neutral'); } else if (isValid) { this.setInputState(input, 'valid'); } else { this.setInputState(input, 'invalid', 'Must be a valid HTTP/HTTPS URL'); } return isValid; } setInputState(input, state, message = '') { const formGroup = input.closest('.form-group'); const feedback = formGroup.querySelector('.form-feedback') || this.createFeedbackElement(formGroup); // Remove existing state classes input.classList.remove('is-valid', 'is-invalid'); feedback.classList.remove('valid-feedback', 'invalid-feedback'); switch (state) { case 'valid': input.classList.add('is-valid'); feedback.classList.add('valid-feedback'); feedback.textContent = 'Looks good!'; feedback.style.display = 'block'; break; case 'invalid': input.classList.add('is-invalid'); feedback.classList.add('invalid-feedback'); feedback.textContent = message; feedback.style.display = 'block'; break; case 'neutral': default: feedback.style.display = 'none'; break; } } createFeedbackElement(formGroup) { const feedback = document.createElement('div'); feedback.className = 'form-feedback'; feedback.style.fontSize = '0.875rem'; feedback.style.marginTop = '0.25rem'; formGroup.appendChild(feedback); return feedback; } async loadSystemStatus() { try { const response = await fetch('dashboard.php?action=get_status', { method: 'GET', headers: { 'X-Requested-With': 'XMLHttpRequest' } }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); this.updateSystemStatus(data); } catch (error) { console.error('Failed to load system status:', error); this.showAlert('Failed to load system status', 'error'); } } updateSystemStatus(status) { // Update hostname const hostnameElement = document.getElementById('hostname'); if (hostnameElement) { hostnameElement.textContent = status.hostname || 'Unknown'; } // Update Ziti status const zitiStatusElement = document.getElementById('zitiStatus'); const zitiStatusIcon = document.getElementById('zitiStatusIcon'); if (zitiStatusElement && zitiStatusIcon) { if (status.ziti_status === 'installed') { zitiStatusElement.textContent = `Installed (${status.ziti_version})`; zitiStatusIcon.className = 'status-icon success'; zitiStatusIcon.innerHTML = '✓'; } else { zitiStatusElement.textContent = 'Not Installed'; zitiStatusIcon.className = 'status-icon error'; zitiStatusIcon.innerHTML = '✗'; } } // Update service status const serviceStatusElement = document.getElementById('serviceStatus'); const serviceStatusIcon = document.getElementById('serviceStatusIcon'); if (serviceStatusElement && serviceStatusIcon) { if (status.service_active) { serviceStatusElement.textContent = 'Running'; serviceStatusIcon.className = 'status-icon success'; serviceStatusIcon.innerHTML = '▶'; } else { serviceStatusElement.textContent = 'Stopped'; serviceStatusIcon.className = 'status-icon error'; serviceStatusIcon.innerHTML = '⏹'; } } // Update configuration status const configStatusElement = document.getElementById('configStatus'); const configStatusIcon = document.getElementById('configStatusIcon'); if (configStatusElement && configStatusIcon) { if (status.config_exists && status.certificates_exist) { configStatusElement.textContent = 'Configured'; configStatusIcon.className = 'status-icon success'; configStatusIcon.innerHTML = '⚙'; } else if (status.config_exists) { configStatusElement.textContent = 'Partial'; configStatusIcon.className = 'status-icon warning'; configStatusIcon.innerHTML = '⚠'; } else { configStatusElement.textContent = 'Not Configured'; configStatusIcon.className = 'status-icon error'; configStatusIcon.innerHTML = '✗'; } } } simulateProgress() { const steps = [ { step: 0, message: 'Initializing enrollment process...', delay: 800 }, { step: 1, message: 'Verifying OpenZiti installation and requirements...', delay: 2500 }, { step: 3, message: 'Creating necessary directories...', delay: 1800 }, { step: 4, message: 'Registering router with ZitiNexus Portal...', delay: 3500 }, { step: 5, message: 'Saving configuration files and certificates...', delay: 2200 }, { step: 6, message: 'Enrolling router with OpenZiti controller...', delay: 4000 }, { step: 7, message: 'Creating and configuring systemd service...', delay: 2000 }, { step: 8, message: 'Starting router service...', delay: 2800 }, { step: 9, message: 'Reporting enrollment status...', delay: 1500 } ]; let currentIndex = 0; this.progressSimulationActive = true; const advanceStep = () => { if (currentIndex < steps.length && this.enrollmentInProgress && this.progressSimulationActive) { const { step, message, delay } = steps[currentIndex]; this.currentStep = step; // Calculate percentage based on step progress const percentage = Math.round(((step + 1) / 11) * 90); // Cap at 90% until real completion this.updateProgress(percentage, message); currentIndex++; setTimeout(advanceStep, delay); } }; // Start simulation after a brief delay setTimeout(advanceStep, 300); } stopProgressSimulation() { this.progressSimulationActive = false; } async startEnrollment() { if (this.enrollmentInProgress) { return; } const hashKeyInput = document.getElementById('hashKey'); const apiEndpointInput = document.getElementById('apiEndpoint'); const enrollBtn = document.getElementById('enrollBtn'); // Validate inputs const hashKeyValid = this.validateHashKey(hashKeyInput); const apiEndpointValid = this.validateApiEndpoint(apiEndpointInput); if (!hashKeyValid || !apiEndpointValid) { this.showAlert('Please fix the validation errors before proceeding', 'error'); return; } // Start enrollment process this.enrollmentInProgress = true; this.currentStep = 0; // Update UI enrollBtn.disabled = true; enrollBtn.innerHTML = 'Starting Enrollment...'; this.showProgressContainer(); this.clearLogs(); // Start progress simulation immediately this.simulateProgress(); try { const formData = new FormData(); formData.append('action', 'enroll'); formData.append('hashKey', hashKeyInput.value.trim()); formData.append('apiEndpoint', apiEndpointInput.value.trim()); formData.append('csrf_token', document.querySelector('input[name="csrf_token"]').value); const response = await fetch('dashboard.php', { method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' } }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const result = await response.json(); // Stop progress simulation this.stopProgressSimulation(); if (result.success) { // Complete the progress this.currentStep = 10; // Final step this.updateProgress(100, 'Enrollment completed successfully!'); this.addLogEntry('success', `Router '${result.routerName}' enrolled successfully`); this.showAlert(`Router enrollment completed successfully! Router: ${result.routerName}`, 'success'); // Refresh system status after successful enrollment setTimeout(() => { this.loadSystemStatus(); }, 2000); } else { throw new Error(result.error || 'Enrollment failed'); } } catch (error) { console.error('Enrollment failed:', error); // Stop progress simulation and show error this.stopProgressSimulation(); this.updateProgress(null, 'Enrollment failed'); this.addLogEntry('error', `Enrollment failed: ${error.message}`); this.showAlert(`Enrollment failed: ${error.message}`, 'error'); } finally { this.enrollmentInProgress = false; enrollBtn.disabled = false; enrollBtn.innerHTML = 'Start Enrollment'; } } showProgressContainer() { const progressContainer = document.getElementById('progressContainer'); if (progressContainer) { progressContainer.classList.add('active'); } } updateProgress(percentage, message) { // Update progress bar const progressFill = document.getElementById('progressFill'); if (progressFill && percentage !== null) { progressFill.style.width = `${percentage}%`; } // Update current step if (message) { this.addLogEntry('info', message); } // Update progress steps this.updateProgressSteps(); } updateProgressSteps() { const progressSteps = document.querySelectorAll('.progress-step'); progressSteps.forEach((step, index) => { step.classList.remove('active', 'completed', 'error'); if (index < this.currentStep) { step.classList.add('completed'); } else if (index === this.currentStep) { step.classList.add('active'); } }); } addLogEntry(type, message) { const logContainer = document.getElementById('logContainer'); if (!logContainer) return; const timestamp = new Date().toLocaleTimeString(); const logEntry = document.createElement('div'); logEntry.className = `log-entry ${type}`; logEntry.textContent = `[${timestamp}] ${message}`; logContainer.appendChild(logEntry); logContainer.scrollTop = logContainer.scrollHeight; } clearLogs() { const logContainer = document.getElementById('logContainer'); if (logContainer) { logContainer.innerHTML = ''; } } showAlert(message, type = 'info') { // Remove existing alerts const existingAlerts = document.querySelectorAll('.alert'); existingAlerts.forEach(alert => alert.remove()); // Create new alert const alert = document.createElement('div'); alert.className = `alert alert-${type}`; alert.textContent = message; // Insert at the top of main content const mainContent = document.querySelector('.main-content'); if (mainContent) { mainContent.insertBefore(alert, mainContent.firstChild); } // Auto-remove after 5 seconds for non-error alerts if (type !== 'error') { setTimeout(() => { if (alert.parentNode) { alert.remove(); } }, 5000); } } showCleanupConfirmation() { const confirmed = confirm( '⚠️ WARNING: Router Cleanup\n\n' + 'This action will:\n' + '• Stop and disable the ziti-router service\n' + '• Remove all router configuration files\n' + '• Delete all certificates\n' + '• Remove the systemd service file\n\n' + 'This action cannot be undone!\n\n' + 'Are you sure you want to proceed?' ); if (confirmed) { this.startCleanup(); } } simulateCleanupProgress() { const cleanupSteps = [ { step: 0, message: 'Starting router cleanup process...', delay: 500 }, { step: 2, message: 'Stopping ziti-router service...', delay: 1500 }, { step: 4, message: 'Resetting service failed state...', delay: 1000 }, { step: 6, message: 'Cleaning service state...', delay: 1200 }, { step: 8, message: 'Removing router configuration directory...', delay: 2000 }, { step: 9, message: 'Removing systemd service file...', delay: 1000 } ]; let currentIndex = 0; this.cleanupSimulationActive = true; const advanceStep = () => { if (currentIndex < cleanupSteps.length && this.cleanupSimulationActive) { const { step, message, delay } = cleanupSteps[currentIndex]; this.currentStep = step; // Calculate percentage based on cleanup progress const percentage = Math.round(((step + 1) / 11) * 85); // Cap at 85% until real completion this.updateProgress(percentage, message); currentIndex++; setTimeout(advanceStep, delay); } }; // Start cleanup simulation after a brief delay setTimeout(advanceStep, 200); } stopCleanupSimulation() { this.cleanupSimulationActive = false; } async startCleanup() { if (this.enrollmentInProgress) { this.showAlert('Cannot perform cleanup while enrollment is in progress', 'error'); return; } const cleanupBtn = document.getElementById('cleanupBtn'); // Update UI cleanupBtn.disabled = true; cleanupBtn.innerHTML = 'Cleaning Up...'; this.showProgressContainer(); this.clearLogs(); this.currentStep = 0; // Start cleanup progress simulation this.simulateCleanupProgress(); try { const formData = new FormData(); formData.append('action', 'cleanup'); formData.append('csrf_token', document.querySelector('input[name="csrf_token"]').value); const response = await fetch('dashboard.php', { method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' } }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const result = await response.json(); // Stop cleanup simulation this.stopCleanupSimulation(); if (result.success) { // Complete the cleanup progress this.currentStep = 10; // Final step this.updateProgress(100, 'Cleanup completed successfully!'); this.addLogEntry('success', result.message); this.showAlert('Router cleanup completed successfully! The page will reload in 3 seconds.', 'success'); // Clear the hash key input field const hashKeyInput = document.getElementById('hashKey'); if (hashKeyInput) { hashKeyInput.value = ''; this.setInputState(hashKeyInput, 'neutral'); } // Refresh system status immediately this.loadSystemStatus(); // Auto-reload page after 3 seconds setTimeout(() => { window.location.reload(); }, 3000); } else { throw new Error(result.error || 'Cleanup failed'); } } catch (error) { console.error('Cleanup failed:', error); // Stop cleanup simulation and show error this.stopCleanupSimulation(); this.updateProgress(null, 'Cleanup failed'); this.addLogEntry('error', `Cleanup failed: ${error.message}`); this.showAlert(`Cleanup failed: ${error.message}`, 'error'); } finally { cleanupBtn.disabled = false; cleanupBtn.innerHTML = '🗑️ Clean Up Router'; } } // Utility method to format file sizes formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } // Utility method to format timestamps formatTimestamp(timestamp) { return new Date(timestamp * 1000).toLocaleString(); } } // Initialize the application when DOM is loaded document.addEventListener('DOMContentLoaded', () => { window.enrollmentUI = new EnrollmentUI(); }); // Add CSS classes for input validation const style = document.createElement('style'); style.textContent = ` .form-input.is-valid { border-color: var(--success-color); box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1); } .form-input.is-invalid { border-color: var(--error-color); box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1); } .valid-feedback { color: var(--success-color); } .invalid-feedback { color: var(--error-color); } `; document.head.appendChild(style);