/* jshint esversion: 2024, module: true */ import { ApiClient } from "./api-client.js"; import { ConfigManager } from "./config-manager.js"; import { EditorMode, 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 { #elements; #state; #themeManager; #structuredEditor; /** * @param {Object} elements - DOM elements */ constructor(elements) { this.#elements = elements; this.#state = { currentInterface: null, interfaces: [], editorMode: EditorMode.RAW, currentConfigFile: null, }; // 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 * @private */ #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 * @private */ #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 * @private */ #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 * @private * @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 * @private */ 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 * @private * @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 * @private */ 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 * @private */ 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 * @private */ 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 * @private * @param {Object} detail - Event detail */ #handleAddSection(detail) { console.log("Add section:", detail); // TODO: Implement section addition logic } /** * Handle remove section from structured editor * @private * @param {Object} detail - Event detail */ #handleRemoveSection(detail) { console.log("Remove section:", detail); // TODO: Implement section removal logic } /** * Handle validate configuration * @private */ 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 * @private * @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 * @private */ #clearValidationResult() { this.#elements.outputs.validateResult.textContent = ""; this.#elements.outputs.validateResult.className = ""; } /** * Set editor mode (raw or structured) * @private * @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 * @private */ 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}`); } } /** * Load and display network status * @private */ 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 * @private * @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 * @private */ 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 * @private */ 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 * @private */ 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}`; } } /** * 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]?.(); } /** * Get current application state (for debugging) * @method getState * @returns {Object} Application state */ getState() { return { ...this.#state }; } /** * Get theme manager instance * @method getThemeManager * @returns {ThemeManager} Theme manager instance */ getThemeManager() { return this.#themeManager; } } // 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 };