summaryrefslogtreecommitdiffstats
path: root/static/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'static/app.js')
-rw-r--r--static/app.js1185
1 files changed, 636 insertions, 549 deletions
diff --git a/static/app.js b/static/app.js
index 22e52b5..3fdd32e 100644
--- a/static/app.js
+++ b/static/app.js
@@ -1,61 +1,65 @@
/* 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';
+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 {
- /**
- * @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 = `
+ #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 = `
<div class="editor-mode-toggle" style="margin-bottom: var(--spacing-l);">
- <button class="button small ${this.state.editorMode === EditorMode.RAW ? 'active' : ''}"
+ <button class="button small ${this.#state.editorMode === EditorMode.RAW ? "active" : ""}"
data-mode="raw" id="rawEditorBtn">
📝 Raw Editor
</button>
- <button class="button small ${this.state.editorMode === EditorMode.STRUCTURED ? 'active' : ''}"
+ <button class="button small ${this.#state.editorMode === EditorMode.STRUCTURED ? "active" : ""}"
data-mode="structured" id="structuredEditorBtn">
🏗️ Structured Editor
</button>
@@ -68,512 +72,595 @@ class Application {
</div>
`;
- // 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 =
- `<div class="error-message">Error loading status: ${error.message}</div>`;
- }
- }
-
- /**
- * 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}`;
- }
- }
+ // 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 = `<div class="error-message">Error loading status: ${error.message}</div>`;
+ }
+ }
+
+ /**
+ * 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;
+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 };