/* jshint esversion: 2024, module: true */ import { ApiClient } from './api-client.js'; import { ConfigManager } from './config-manager.js'; import { EditorMode, ThemeMode, ValidationState } from './enums.js'; import { InterfaceRenderer } from './interface-renderer.js'; import { StructuredEditor } from './structured-editor.js'; import { ThemeManager } from './theme-manager.js'; /** * Main Application Class * @class Application */ class Application { /** * @param {Object} elements - DOM elements */ constructor(elements) { this.elements = elements; this.state = { currentInterface: null, interfaces: [], editorMode: EditorMode.STRUCTURED, currentConfigFile: null, theme: ThemeMode.DARK }; // Initialize modules this.themeManager = new ThemeManager(elements); this.structuredEditor = null; // Create editor mode toggle UI this.createEditorModeToggle(); } /** * Initialize the application * @method init */ init() { this.themeManager.init(); this.setupEventListeners(); this.loadStatus(); this.initializeStructuredEditor(); } /** * Create editor mode toggle UI * @method createEditorModeToggle */ createEditorModeToggle() { const editorToggleHTML = `
`; // Insert the toggle and containers into the configs panel const configCard = this.elements.panels.configs.querySelector('.card:last-child'); configCard.insertAdjacentHTML('afterbegin', editorToggleHTML); // Move existing form elements to raw editor container const rawEditorContainer = document.getElementById('rawEditorContainer'); const formGroups = Array.from(configCard.querySelectorAll('.form-group, .checkbox-group')); const configActions = configCard.querySelector('.config-actions') || configCard.querySelector('div:has(> #validateConfig)'); formGroups.forEach(group => { if (!group.closest('.editor-mode-toggle')) { rawEditorContainer.appendChild(group); } }); // Move config actions if they exist if (configActions) { rawEditorContainer.appendChild(configActions); } // Update elements reference this.elements.editorContainers = { raw: document.getElementById('rawEditorContainer'), structured: document.getElementById('structuredEditorContainer') }; this.elements.editorButtons = { raw: document.getElementById('rawEditorBtn'), structured: document.getElementById('structuredEditorBtn') }; } /** * Initialize structured editor * @method initializeStructuredEditor */ initializeStructuredEditor() { if (this.elements.editorContainers.structured) { this.structuredEditor = new StructuredEditor(this.elements.editorContainers.structured); } else { console.error('Structured editor container not found'); } } /** * Set up all event listeners * @method setupEventListeners */ setupEventListeners() { // Navigation this.elements.buttons.nav.forEach(button => { button.addEventListener('click', (event) => { this.show(event.currentTarget.dataset.panel); }); }); // Status panel this.elements.buttons.refreshStatus?.addEventListener('click', () => this.loadStatus()); // Configs panel - raw editor this.elements.buttons.refreshConfigs?.addEventListener('click', () => this.handleRefreshConfigs()); this.elements.buttons.saveConfig?.addEventListener('click', () => this.handleSaveConfig()); this.elements.buttons.validateConfig?.addEventListener('click', () => this.handleValidateConfig()); this.elements.inputs.configSelect?.addEventListener('change', () => this.handleConfigFileChange()); // Editor mode toggle this.elements.editorButtons?.raw?.addEventListener('click', () => this.setEditorMode(EditorMode.RAW)); this.elements.editorButtons?.structured?.addEventListener('click', () => this.setEditorMode(EditorMode.STRUCTURED)); // Logs panel this.elements.buttons.refreshLogs?.addEventListener('click', () => this.loadLogs()); // Commands panel this.elements.buttons.restartNetworkd?.addEventListener('click', () => this.restartNetworkd()); this.elements.buttons.rebootDevice?.addEventListener('click', () => this.rebootDevice()); // Touch support document.addEventListener('touchstart', this.handleTouchStart, { passive: true }); } /** * Handle touch events for better mobile support * @method handleTouchStart * @param {TouchEvent} event */ handleTouchStart = (event) => { // Add visual feedback for touch if (event.target.classList.contains('button') || event.target.classList.contains('nav-button')) { event.target.style.opacity = '0.7'; setTimeout(() => { event.target.style.opacity = ''; }, 150); } }; /** * Handle refresh configuration files * @method handleRefreshConfigs */ async handleRefreshConfigs() { try { const files = await ConfigManager.refreshConfigs(); this.updateConfigSelect(files); if (files.length > 0) { this.state.currentConfigFile = files[0]; await this.handleConfigFileChange(); } else { this.elements.inputs.cfgEditor.value = ''; this.state.currentConfigFile = null; } } catch (error) { alert(error.message); } } /** * Update configuration select element * @method updateConfigSelect * @param {Array} files - File names */ updateConfigSelect(files) { this.elements.inputs.configSelect.innerHTML = ''; files.forEach(file => { const option = new Option(file, file); this.elements.inputs.configSelect.add(option); }); } /** * Handle configuration file change * @method handleConfigFileChange */ async handleConfigFileChange() { const name = this.elements.inputs.configSelect.value; if (!name) return; this.state.currentConfigFile = name; if (this.state.editorMode === EditorMode.RAW) { await this.loadConfigForRawEditor(); } else { await this.loadConfigForStructuredEditor(); } } /** * Load config for raw editor * @method loadConfigForRawEditor */ async loadConfigForRawEditor() { try { const content = await ConfigManager.loadConfig(this.state.currentConfigFile); this.elements.inputs.cfgEditor.value = content; this.clearValidationResult(); } catch (error) { alert(error.message); } } /** * Load config for structured editor * @method loadConfigForStructuredEditor */ async loadConfigForStructuredEditor() { if (!this.structuredEditor) { console.error('Structured editor not initialized'); return; } try { const content = await ConfigManager.loadConfig(this.state.currentConfigFile); // Parse configuration and get schema const { NetworkConfiguration } = await import('./systemd-network.js'); const config = NetworkConfiguration.fromSystemdConfiguration(content); const schema = config.getSchema(); // Load schema into structured editor this.structuredEditor.loadSchema(schema, this.state.currentConfigFile); // Set up event listeners for structured editor this.structuredEditor.on('addSection', (event) => this.handleAddSection(event.detail)); this.structuredEditor.on('removeSection', (event) => this.handleRemoveSection(event.detail)); } catch (error) { alert(`Failed to load config for structured editor: ${error.message}`); } } /** * Handle add section from structured editor * @method handleAddSection * @param {Object} detail - Event detail */ handleAddSection(detail) { console.log('Add section:', detail); // TODO: Implement section addition logic // This would involve updating the schema and re-rendering } /** * Handle remove section from structured editor * @method handleRemoveSection * @param {Object} detail - Event detail */ handleRemoveSection(detail) { console.log('Remove section:', detail); // TODO: Implement section removal logic // This would involve updating the schema and re-rendering } /** * Handle validate configuration * @method handleValidateConfig */ async handleValidateConfig() { const name = this.state.currentConfigFile; if (!name) { alert('Please select a configuration file first.'); return; } let content; if (this.state.editorMode === EditorMode.RAW) { content = this.elements.inputs.cfgEditor.value; } else if (this.structuredEditor) { content = this.structuredEditor.getConfigurationText(); } else { alert('Structured editor not available'); return; } this.setValidationResult(ValidationState.PENDING); try { const result = await ConfigManager.validateConfig(name, content); if (result.ok) { this.setValidationResult(ValidationState.SUCCESS); } else { this.setValidationResult(ValidationState.ERROR, result.output); } } catch (error) { this.setValidationResult(ValidationState.ERROR, error.message); } } /** * Set validation result * @method setValidationResult * @param {Symbol} state - Validation state * @param {string} [message] - Additional message */ setValidationResult(state, message = '') { this.elements.outputs.validateResult.textContent = ConfigManager.getValidationMessage(state, message); this.elements.outputs.validateResult.className = ConfigManager.getValidationClass(state); } /** * Clear validation result * @method clearValidationResult */ clearValidationResult() { this.elements.outputs.validateResult.textContent = ''; this.elements.outputs.validateResult.className = ''; } /** * Set editor mode (raw or structured) * @method setEditorMode * @param {Symbol} mode - Editor mode */ setEditorMode(mode) { this.state.editorMode = mode; // Update UI if (this.elements.editorButtons?.raw && this.elements.editorButtons?.structured) { this.elements.editorButtons.raw.classList.toggle('active', mode === EditorMode.RAW); this.elements.editorButtons.structured.classList.toggle('active', mode === EditorMode.STRUCTURED); } if (this.elements.editorContainers?.raw && this.elements.editorContainers?.structured) { this.elements.editorContainers.raw.style.display = mode === EditorMode.RAW ? 'block' : 'none'; this.elements.editorContainers.structured.style.display = mode === EditorMode.STRUCTURED ? 'block' : 'none'; } // If switching to structured mode and we have a config file loaded, load it if (mode === EditorMode.STRUCTURED && this.state.currentConfigFile && this.structuredEditor) { this.loadConfigForStructuredEditor(); } } /** * Handle save configuration based on current editor mode * @method handleSaveConfig */ async handleSaveConfig() { const name = this.state.currentConfigFile; if (!name) { alert('Please select a configuration file first.'); return; } const restart = this.elements.inputs.restartAfterSave.checked; if (!confirm(`Save file ${name}? This will create a backup and ${restart ? 'restart' : 'not restart'} networkd.`)) { return; } try { let content; if (this.state.editorMode === EditorMode.RAW) { content = this.elements.inputs.cfgEditor.value; } else if (this.structuredEditor) { content = this.structuredEditor.getConfigurationText(); } else { throw new Error('Structured editor not available'); } const result = await ConfigManager.saveConfig(name, content, restart); alert(`Saved: ${result.status ?? 'ok'}`); // Refresh the config in structured editor if needed if (this.state.editorMode === EditorMode.STRUCTURED && this.structuredEditor) { await this.loadConfigForStructuredEditor(); } } catch (error) { alert(`Save failed: ${error.message}`); } } /** * Show specified panel and hide others * @method show * @param {string} panel - Panel to show */ show(panel) { // Hide all panels and remove active class from buttons Object.values(this.elements.panels).forEach(p => { if (p) p.classList.remove('active'); }); this.elements.buttons.nav.forEach(btn => { if (btn) btn.classList.remove('active'); }); // Show selected panel and activate button const targetPanel = this.elements.panels[panel]; const targetButton = document.querySelector(`[data-panel="${panel}"]`); if (targetPanel) targetPanel.classList.add('active'); if (targetButton) targetButton.classList.add('active'); // Load panel-specific data const panelActions = { status: () => this.loadStatus(), configs: () => this.handleRefreshConfigs(), logs: () => this.loadLogs(), }; panelActions[panel]?.(); } /** * Load and display network status * @method loadStatus */ async loadStatus() { try { const data = await ApiClient.getNetworkStatus(); this.state.interfaces = data.Interfaces ?? []; // Render interface tabs const tabsHTML = InterfaceRenderer.renderInterfaceTabs(this.state.interfaces, this.state.currentInterface); this.elements.outputs.ifaceTabs.innerHTML = tabsHTML; // Show first interface by default if (this.state.interfaces.length > 0 && !this.state.currentInterface) { this.showInterfaceDetails(this.state.interfaces[0]); } // Add event listeners to tabs this.elements.outputs.ifaceTabs.querySelectorAll('.interface-tab').forEach(tab => { tab.addEventListener('click', (event) => { const ifaceName = event.currentTarget.dataset.interface; const iface = this.state.interfaces.find(i => i.Name === ifaceName); if (iface) { this.showInterfaceDetails(iface); } }); }); } catch (error) { this.elements.outputs.ifaceDetails.innerHTML = `
Error loading status: ${error.message}
`; } } /** * Show interface details * @method showInterfaceDetails * @param {Object} iface - Interface object */ showInterfaceDetails(iface) { this.state.currentInterface = iface; // Update active tab this.elements.outputs.ifaceTabs.querySelectorAll('.interface-tab').forEach(tab => { tab.classList.toggle('active', tab.dataset.interface === iface.Name); }); const detailsHTML = InterfaceRenderer.showInterfaceDetails(iface); this.elements.outputs.ifaceDetails.innerHTML = detailsHTML; } /** * Load system logs * @method loadLogs */ async loadLogs() { try { const text = await ApiClient.getSystemLogs(); this.elements.outputs.logsArea.textContent = text; } catch (error) { this.elements.outputs.logsArea.textContent = `Error: ${error.message}`; } } /** * Restart networkd service * @method restartNetworkd */ async restartNetworkd() { if (!confirm('Restart systemd-networkd? Active connections may be reset.')) return; try { const result = await ApiClient.restartNetworkd(); this.elements.outputs.cmdResult.textContent = `Success: ${JSON.stringify(result)}`; } catch (error) { this.elements.outputs.cmdResult.textContent = `Error: ${error.message}`; } } /** * Reboot the device * @method rebootDevice */ async rebootDevice() { if (!confirm('Reboot device now?')) return; try { const result = await ApiClient.rebootDevice(); this.elements.outputs.cmdResult.textContent = `Success: ${JSON.stringify(result)}`; } catch (error) { this.elements.outputs.cmdResult.textContent = `Error: ${error.message}`; } } } // Initialize application when DOM is loaded document.addEventListener('DOMContentLoaded', () => { const elements = { themeToggle: document.getElementById('themeToggle'), themeIcon: document.getElementById('themeIcon'), panels: { status: document.getElementById('panelStatus'), configs: document.getElementById('panelConfigs'), logs: document.getElementById('panelLogs'), commands: document.getElementById('panelCommands'), }, buttons: { nav: document.querySelectorAll('.nav-button'), refreshStatus: document.getElementById('refreshStatus'), refreshConfigs: document.getElementById('refreshConfigs'), saveConfig: document.getElementById('saveConfig'), validateConfig: document.getElementById('validateConfig'), refreshLogs: document.getElementById('refreshLogs'), restartNetworkd: document.getElementById('restartNetworkd'), rebootDevice: document.getElementById('rebootDevice'), }, inputs: { configSelect: document.getElementById('configSelect'), cfgEditor: document.getElementById('cfgEditor'), restartAfterSave: document.getElementById('restartAfterSave'), }, outputs: { ifaceTabs: document.getElementById('interfaceTabs'), ifaceDetails: document.getElementById('interfaceDetails'), validateResult: document.getElementById('validateResult'), logsArea: document.getElementById('logsArea'), cmdResult: document.getElementById('cmdResult'), }, }; const app = new Application(elements); app.init(); // Make app globally available for debugging window.app = app; }); export { Application };