zitinexus-router-script/UI/public/assets/js/app.js

589 lines
20 KiB
JavaScript

/**
* 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.progressPollingInterval = null;
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.refreshSystemStatus();
});
}
// Clear logs button
const clearLogsBtn = document.getElementById('clearLogs');
if (clearLogsBtn) {
clearLogsBtn.addEventListener('click', () => {
this.clearLogs();
});
}
}
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');
}
}
async refreshSystemStatus() {
const refreshBtn = document.getElementById('refreshStatus');
const originalText = refreshBtn.innerHTML;
try {
// Show loading state
refreshBtn.disabled = true;
refreshBtn.innerHTML = '🔄 Refreshing...';
const response = await fetch('dashboard.php?action=refresh_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);
this.showAlert('System status refreshed successfully', 'success');
} catch (error) {
console.error('Failed to refresh system status:', error);
this.showAlert('Failed to refresh system status', 'error');
} finally {
// Restore button state
refreshBtn.disabled = false;
refreshBtn.innerHTML = originalText;
}
}
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 = '✗';
}
}
}
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 = '<span class="spinner"></span>Starting Enrollment...';
this.showProgressContainer();
this.clearLogs();
// Clear any existing progress
await this.clearProgress();
// Start progress polling
this.startProgressPolling();
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);
// Start the enrollment process (this will run in background)
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();
if (result.success) {
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);
this.addLogEntry('error', `Enrollment failed: ${error.message}`);
this.showAlert(`Enrollment failed: ${error.message}`, 'error');
this.stopProgressPolling();
} finally {
this.enrollmentInProgress = false;
enrollBtn.disabled = false;
enrollBtn.innerHTML = 'Start Enrollment';
}
}
/**
* Start polling for progress updates
*/
startProgressPolling() {
// Clear any existing polling
this.stopProgressPolling();
// Poll every 500ms for progress updates
this.progressPollingInterval = setInterval(async () => {
try {
const response = await fetch('progress.php', {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
if (response.ok) {
const progress = await response.json();
this.updateProgressFromPolling(progress);
// Stop polling if enrollment is complete or failed
if (progress.status === 'completed' || progress.status === 'error') {
this.stopProgressPolling();
}
}
} catch (error) {
console.error('Progress polling error:', error);
}
}, 500);
}
/**
* Stop progress polling
*/
stopProgressPolling() {
if (this.progressPollingInterval) {
clearInterval(this.progressPollingInterval);
this.progressPollingInterval = null;
}
}
/**
* Update UI based on progress polling data
*/
updateProgressFromPolling(progress) {
// Update progress bar
const progressFill = document.getElementById('progressFill');
if (progressFill && progress.percentage !== undefined) {
progressFill.style.width = `${progress.percentage}%`;
}
// Update current step index
this.currentStep = progress.current_step_index || 0;
// Update progress steps
this.updateProgressStepsFromData(progress);
// Add new log entries
if (progress.logs && Array.isArray(progress.logs)) {
this.updateLogsFromData(progress.logs);
}
// Handle completion or error
if (progress.status === 'completed') {
this.addLogEntry('success', 'Enrollment completed successfully!');
} else if (progress.status === 'error') {
this.addLogEntry('error', progress.error || 'Enrollment failed');
}
}
/**
* Update progress steps based on polling data
*/
updateProgressStepsFromData(progress) {
const progressSteps = document.querySelectorAll('.progress-step');
const stepNames = ['INIT', 'REQUIREMENTS', 'INSTALL', 'DIRECTORIES', 'REGISTER', 'CONFIG', 'ENROLL', 'SERVICE', 'START', 'REPORT', 'COMPLETE'];
progressSteps.forEach((stepElement, index) => {
stepElement.classList.remove('active', 'completed', 'error');
const stepName = stepNames[index];
if (progress.completed_steps && progress.completed_steps.includes(stepName)) {
stepElement.classList.add('completed');
} else if (progress.current_step_index === index) {
stepElement.classList.add('active');
}
if (progress.status === 'error' && progress.current_step_index === index) {
stepElement.classList.add('error');
}
});
}
/**
* Update logs from polling data (only add new entries)
*/
updateLogsFromData(logs) {
const logContainer = document.getElementById('logContainer');
if (!logContainer) return;
// Get current log count to avoid duplicates
const currentLogCount = logContainer.children.length;
// Add only new log entries
for (let i = currentLogCount; i < logs.length; i++) {
const log = logs[i];
const logEntry = document.createElement('div');
logEntry.className = `log-entry ${log.type}`;
logEntry.textContent = `[${log.timestamp}] ${log.message}`;
logContainer.appendChild(logEntry);
}
// Auto-scroll to bottom
logContainer.scrollTop = logContainer.scrollHeight;
}
/**
* Clear progress data
*/
async clearProgress() {
try {
await fetch('dashboard.php?action=clear_progress', {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
} catch (error) {
console.error('Failed to clear progress:', error);
}
}
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);
}
}
// 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);