/* jshint esversion: 2024, module: true */ import { ThemeManager } from './theme-manager.js'; import { ApiClient } from './api-client.js'; import { InterfaceRenderer } from './interface-renderer.js'; import { ConfigManager } from './config-manager.js'; import { StructuredEditor } from './structured-editor.js'; /** * Main Application Class * @class Application */ class Application { /** * @param {Object} elements - DOM elements */ constructor(elements) { this.elements = elements; this.state = { currentInterface: null, interfaces: [], editorMode: 'raw', // 'raw' or 'structured' currentConfigFile: null }; // Initialize modules this.themeManager = new ThemeManager(elements); this.apiClient = new ApiClient(); this.interfaceRenderer = new InterfaceRenderer(elements, this.state); this.configManager = new ConfigManager(elements, this.apiClient, this.state); // Structured editor will be initialized after DOM is ready this.structuredEditor = null; // Create editor mode toggle UI this.createEditorModeToggle(); } /** * Initialize the application * @method init */ init() { this.themeManager.init(); this.setupEventListeners(); this.loadStatus(); // Initialize structured editor now that DOM is ready 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.configManager.refreshConfigs()); this.elements.buttons.saveConfig?.addEventListener('click', () => this.handleSaveConfig()); this.elements.buttons.validateConfig?.addEventListener('click', () => this.configManager.validateConfig()); this.elements.inputs.configSelect?.addEventListener('change', () => this.handleConfigFileChange()); // Editor mode toggle this.elements.editorButtons?.raw?.addEventListener('click', () => this.setEditorMode('raw')); this.elements.editorButtons?.structured?.addEventListener('click', () => this.setEditorMode('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 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 === 'raw') { await this.configManager.loadConfig(); } else { await this.loadConfigForStructuredEditor(); } } /** * Load config for structured editor * @method loadConfigForStructuredEditor */ async loadConfigForStructuredEditor() { if (!this.structuredEditor) { console.error('Structured editor not initialized'); return; } try { const text = await this.apiClient.getText(`/api/config/${encodeURIComponent(this.state.currentConfigFile)}`); await this.structuredEditor.loadConfiguration(text, this.state.currentConfigFile); } catch (error) { alert(`Failed to load config for structured editor: ${error.message}`); } } /** * Set editor mode (raw or structured) * @method setEditorMode * @param {string} 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 === 'raw'); this.elements.editorButtons.structured.classList.toggle('active', mode === 'structured'); } if (this.elements.editorContainers?.raw && this.elements.editorContainers?.structured) { this.elements.editorContainers.raw.style.display = mode === 'raw' ? 'block' : 'none'; this.elements.editorContainers.structured.style.display = mode === 'structured' ? 'block' : 'none'; } // If switching to structured mode and we have a config file loaded, load it if (mode === '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 === '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 this.apiClient.post('/api/save', { name, content, restart }); alert(`Saved: ${result.status ?? 'ok'}`); // Refresh the config in structured editor if needed if (this.state.editorMode === 'structured' && this.structuredEditor) { await this.structuredEditor.loadConfiguration(content, name); } } 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.configManager.refreshConfigs(), logs: () => this.loadLogs(), }; panelActions[panel]?.(); } /** * Load and display network status * @method loadStatus */ async loadStatus() { try { const data = await this.apiClient.get('/api/status'); this.state.interfaces = data.Interfaces ?? []; this.interfaceRenderer.renderInterfaceTabs(this.state.interfaces); // Show first interface by default if (this.state.interfaces.length > 0 && !this.state.currentInterface) { this.interfaceRenderer.showInterfaceDetails(this.state.interfaces[0]); } } catch (error) { this.elements.outputs.ifaceDetails.innerHTML = `
Error loading status: ${error.message}
`; } } /** * Load system logs * @method loadLogs */ async loadLogs() { try { const text = await this.apiClient.getText('/api/logs'); 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 this.apiClient.post('/api/reload'); 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 this.apiClient.post('/api/reboot'); 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 };