summaryrefslogtreecommitdiffstats
path: root/static
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--static/api-client.js280
-rw-r--r--static/app.js1185
-rw-r--r--static/config-manager.js180
-rw-r--r--static/enums.js56
-rw-r--r--static/interface-renderer.js333
-rw-r--r--static/network-types.js180
-rw-r--r--static/structured-editor.js4
-rw-r--r--static/styles.css1026
-rw-r--r--static/systemd-network.js1301
-rw-r--r--static/theme-manager.js218
-rw-r--r--static/utils.js308
11 files changed, 2987 insertions, 2084 deletions
diff --git a/static/api-client.js b/static/api-client.js
index 7a6ffbd..9582dc0 100644
--- a/static/api-client.js
+++ b/static/api-client.js
@@ -1,150 +1,152 @@
/* jshint esversion: 2024, module: true */
-import { ApiEndpoints } from './enums.js';
+import { ApiEndpoints } from "./enums.js";
/**
* Static API Client for network operations
* @class ApiClient
*/
class ApiClient {
- /**
- * API utility function
- * @static
- * @param {string} path - API endpoint
- * @param {Object} [options] - Fetch options
- * @returns {Promise<Response>}
- */
- static async request(path, options = {}) {
- const response = await fetch(path, options);
-
- if (!response.ok) {
- const text = await response.text();
- throw new Error(`${response.status} ${text}`);
- }
-
- return response;
- }
-
- /**
- * GET request returning JSON
- * @static
- * @param {string} path - API endpoint
- * @returns {Promise<Object>}
- */
- static async get(path) {
- const response = await ApiClient.request(path);
- return response.json();
- }
-
- /**
- * GET request returning text
- * @static
- * @param {string} path - API endpoint
- * @returns {Promise<string>}
- */
- static async getText(path) {
- const response = await ApiClient.request(path);
- return response.text();
- }
-
- /**
- * POST request with JSON body
- * @static
- * @param {string} path - API endpoint
- * @param {Object} [data] - Request body
- * @returns {Promise<Object>}
- */
- static async post(path, data = null) {
- const options = {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- };
-
- if (data) {
- options.body = JSON.stringify(data);
- }
-
- const response = await ApiClient.request(path, options);
- return response.json();
- }
-
- /**
- * Get network status
- * @static
- * @returns {Promise<Object>}
- */
- static async getNetworkStatus() {
- return ApiClient.get(ApiEndpoints.STATUS);
- }
-
- /**
- * Get configuration files list
- * @static
- * @returns {Promise<Object>}
- */
- static async getConfigFiles() {
- return ApiClient.get(ApiEndpoints.CONFIGS);
- }
-
- /**
- * Get configuration file content
- * @static
- * @param {string} name - File name
- * @returns {Promise<string>}
- */
- static async getConfigFile(name) {
- return ApiClient.getText(`${ApiEndpoints.CONFIG}/${encodeURIComponent(name)}`);
- }
-
- /**
- * Validate configuration
- * @static
- * @param {string} name - File name
- * @param {string} content - File content
- * @returns {Promise<Object>}
- */
- static async validateConfig(name, content) {
- return ApiClient.post(ApiEndpoints.VALIDATE, { name, content });
- }
-
- /**
- * Save configuration file
- * @static
- * @param {string} name - File name
- * @param {string} content - File content
- * @param {boolean} restart - Restart service
- * @returns {Promise<Object>}
- */
- static async saveConfig(name, content, restart) {
- return ApiClient.post(ApiEndpoints.SAVE, { name, content, restart });
- }
-
- /**
- * Get system logs
- * @static
- * @returns {Promise<string>}
- */
- static async getSystemLogs() {
- return ApiClient.getText(ApiEndpoints.LOGS);
- }
-
- /**
- * Restart networkd service
- * @static
- * @returns {Promise<Object>}
- */
- static async restartNetworkd() {
- return ApiClient.post(ApiEndpoints.RELOAD);
- }
-
- /**
- * Reboot device
- * @static
- * @returns {Promise<Object>}
- */
- static async rebootDevice() {
- return ApiClient.post(ApiEndpoints.REBOOT);
- }
+ /**
+ * API utility function
+ * @static
+ * @param {string} path - API endpoint
+ * @param {Object} [options] - Fetch options
+ * @returns {Promise<Response>}
+ */
+ static async request(path, options = {}) {
+ const response = await fetch(path, options);
+
+ if (!response.ok) {
+ const text = await response.text();
+ throw new Error(`${response.status} ${text}`);
+ }
+
+ return response;
+ }
+
+ /**
+ * GET request returning JSON
+ * @static
+ * @param {string} path - API endpoint
+ * @returns {Promise<Object>}
+ */
+ static async get(path) {
+ const response = await ApiClient.request(path);
+ return response.json();
+ }
+
+ /**
+ * GET request returning text
+ * @static
+ * @param {string} path - API endpoint
+ * @returns {Promise<string>}
+ */
+ static async getText(path) {
+ const response = await ApiClient.request(path);
+ return response.text();
+ }
+
+ /**
+ * POST request with JSON body
+ * @static
+ * @param {string} path - API endpoint
+ * @param {Object} [data] - Request body
+ * @returns {Promise<Object>}
+ */
+ static async post(path, data = null) {
+ const options = {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ };
+
+ if (data) {
+ options.body = JSON.stringify(data);
+ }
+
+ const response = await ApiClient.request(path, options);
+ return response.json();
+ }
+
+ /**
+ * Get network status
+ * @static
+ * @returns {Promise<Object>}
+ */
+ static async getNetworkStatus() {
+ return ApiClient.get(ApiEndpoints.STATUS);
+ }
+
+ /**
+ * Get configuration files list
+ * @static
+ * @returns {Promise<Object>}
+ */
+ static async getConfigFiles() {
+ return ApiClient.get(ApiEndpoints.CONFIGS);
+ }
+
+ /**
+ * Get configuration file content
+ * @static
+ * @param {string} name - File name
+ * @returns {Promise<string>}
+ */
+ static async getConfigFile(name) {
+ return ApiClient.getText(
+ `${ApiEndpoints.CONFIG}/${encodeURIComponent(name)}`,
+ );
+ }
+
+ /**
+ * Validate configuration
+ * @static
+ * @param {string} name - File name
+ * @param {string} content - File content
+ * @returns {Promise<Object>}
+ */
+ static async validateConfig(name, content) {
+ return ApiClient.post(ApiEndpoints.VALIDATE, { name, content });
+ }
+
+ /**
+ * Save configuration file
+ * @static
+ * @param {string} name - File name
+ * @param {string} content - File content
+ * @param {boolean} restart - Restart service
+ * @returns {Promise<Object>}
+ */
+ static async saveConfig(name, content, restart) {
+ return ApiClient.post(ApiEndpoints.SAVE, { name, content, restart });
+ }
+
+ /**
+ * Get system logs
+ * @static
+ * @returns {Promise<string>}
+ */
+ static async getSystemLogs() {
+ return ApiClient.getText(ApiEndpoints.LOGS);
+ }
+
+ /**
+ * Restart networkd service
+ * @static
+ * @returns {Promise<Object>}
+ */
+ static async restartNetworkd() {
+ return ApiClient.post(ApiEndpoints.RELOAD);
+ }
+
+ /**
+ * Reboot device
+ * @static
+ * @returns {Promise<Object>}
+ */
+ static async rebootDevice() {
+ return ApiClient.post(ApiEndpoints.REBOOT);
+ }
}
export { ApiClient };
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 };
diff --git a/static/config-manager.js b/static/config-manager.js
index edb46b2..eea6c91 100644
--- a/static/config-manager.js
+++ b/static/config-manager.js
@@ -1,102 +1,110 @@
/* jshint esversion: 2024, module: true */
-import { ApiClient } from './api-client.js';
-import { ValidationState } from './enums.js';
+import { ApiClient } from "./api-client.js";
+import { ValidationState } from "./enums.js";
/**
* Static Configuration Manager for handling config files
* @class ConfigManager
*/
class ConfigManager {
- /**
- * Refresh configuration file list
- * @static
- * @returns {Promise<Array>} Array of file names
- */
- static async refreshConfigs() {
- try {
- const data = await ApiClient.getConfigFiles();
- return data.files || [];
- } catch (error) {
- throw new Error(`Failed to list configs: ${error.message}`);
- }
- }
+ /**
+ * Refresh configuration file list
+ * @static
+ * @returns {Promise<Array>} Array of file names
+ */
+ static async refreshConfigs() {
+ try {
+ const data = await ApiClient.getConfigFiles();
+ return data.files || [];
+ } catch (error) {
+ throw new Error(`Failed to list configs: ${error.message}`);
+ }
+ }
- /**
- * Load selected configuration file
- * @static
- * @param {string} name - File name
- * @returns {Promise<string>} File content
- */
- static async loadConfig(name) {
- try {
- return await ApiClient.getConfigFile(name);
- } catch (error) {
- throw new Error(`Failed to load: ${error.message}`);
- }
- }
+ /**
+ * Load selected configuration file
+ * @static
+ * @param {string} name - File name
+ * @returns {Promise<string>} File content
+ */
+ static async loadConfig(name) {
+ try {
+ return await ApiClient.getConfigFile(name);
+ } catch (error) {
+ throw new Error(`Failed to load: ${error.message}`);
+ }
+ }
- /**
- * Validate current configuration
- * @static
- * @param {string} name - File name
- * @param {string} content - File content
- * @returns {Promise<Object>} Validation result
- */
- static async validateConfig(name, content) {
- try {
- return await ApiClient.validateConfig(name, content);
- } catch (error) {
- throw new Error(`Validation failed: ${error.message}`);
- }
- }
+ /**
+ * Validate current configuration
+ * @static
+ * @param {string} name - File name
+ * @param {string} content - File content
+ * @returns {Promise<Object>} Validation result
+ */
+ static async validateConfig(name, content) {
+ try {
+ return await ApiClient.validateConfig(name, content);
+ } catch (error) {
+ throw new Error(`Validation failed: ${error.message}`);
+ }
+ }
- /**
- * Save current configuration
- * @static
- * @param {string} name - File name
- * @param {string} content - File content
- * @param {boolean} restart - Restart service
- * @returns {Promise<Object>} Save result
- */
- static async saveConfig(name, content, restart) {
- try {
- return await ApiClient.saveConfig(name, content, restart);
- } catch (error) {
- throw new Error(`Save failed: ${error.message}`);
- }
- }
+ /**
+ * Save current configuration
+ * @static
+ * @param {string} name - File name
+ * @param {string} content - File content
+ * @param {boolean} restart - Restart service
+ * @returns {Promise<Object>} Save result
+ */
+ static async saveConfig(name, content, restart) {
+ try {
+ return await ApiClient.saveConfig(name, content, restart);
+ } catch (error) {
+ throw new Error(`Save failed: ${error.message}`);
+ }
+ }
- /**
- * Get validation state class
- * @static
- * @param {Symbol} state - Validation state
- * @returns {string} CSS class
- */
- static getValidationClass(state) {
- switch (state) {
- case ValidationState.SUCCESS: return 'validation-success';
- case ValidationState.ERROR: return 'validation-error';
- case ValidationState.PENDING: return 'validation-pending';
- default: return '';
- }
- }
+ /**
+ * Get validation state class
+ * @static
+ * @param {Symbol} state - Validation state
+ * @returns {string} CSS class
+ */
+ static getValidationClass(state) {
+ switch (state) {
+ case ValidationState.SUCCESS:
+ return "validation-success";
+ case ValidationState.ERROR:
+ return "validation-error";
+ case ValidationState.PENDING:
+ return "validation-pending";
+ default:
+ return "";
+ }
+ }
- /**
- * Get validation message
- * @static
- * @param {Symbol} state - Validation state
- * @param {string} [message] - Additional message
- * @returns {string} Validation message
- */
- static getValidationMessage(state, message = '') {
- switch (state) {
- case ValidationState.SUCCESS: return '✓ Configuration is valid';
- case ValidationState.ERROR: return `✗ ${message || 'Validation failed'}`;
- case ValidationState.PENDING: return 'Validating...';
- default: return '';
- }
- }
+ /**
+ * Get validation message
+ * @static
+ * @param {Symbol} state - Validation state
+ * @param {string} [message] - Additional message
+ * @returns {string} Validation message
+ */
+ static getValidationMessage(state, message = "") {
+ switch (state) {
+ case ValidationState.SUCCESS:
+ return "✓ Configuration is valid";
+ case ValidationState.ERROR:
+ return `✗ ${message || "Validation failed"}`;
+ case ValidationState.PENDING:
+ return "Validating...";
+ default:
+ return "";
+ }
+ }
}
export { ConfigManager };
diff --git a/static/enums.js b/static/enums.js
index e494e87..c36011c 100644
--- a/static/enums.js
+++ b/static/enums.js
@@ -7,61 +7,61 @@
// Editor modes
const EditorMode = {
- RAW: Symbol("raw"),
- STRUCTURED: Symbol("structured")
+ RAW: Symbol("raw"),
+ STRUCTURED: Symbol("structured"),
};
Object.freeze(EditorMode);
// Panel types
const PanelType = {
- STATUS: Symbol("status"),
- CONFIGS: Symbol("configs"),
- LOGS: Symbol("logs"),
- COMMANDS: Symbol("commands")
+ STATUS: Symbol("status"),
+ CONFIGS: Symbol("configs"),
+ LOGS: Symbol("logs"),
+ COMMANDS: Symbol("commands"),
};
Object.freeze(PanelType);
// Interface states
const InterfaceState = {
- UP: Symbol("up"),
- DOWN: Symbol("down"),
- UNKNOWN: Symbol("unknown")
+ UP: Symbol("up"),
+ DOWN: Symbol("down"),
+ UNKNOWN: Symbol("unknown"),
};
Object.freeze(InterfaceState);
// Theme modes
const ThemeMode = {
- LIGHT: Symbol("light"),
- DARK: Symbol("dark")
+ LIGHT: Symbol("light"),
+ DARK: Symbol("dark"),
};
Object.freeze(ThemeMode);
// Validation states
const ValidationState = {
- PENDING: Symbol("pending"),
- SUCCESS: Symbol("success"),
- ERROR: Symbol("error")
+ PENDING: Symbol("pending"),
+ SUCCESS: Symbol("success"),
+ ERROR: Symbol("error"),
};
Object.freeze(ValidationState);
// API endpoints
const ApiEndpoints = {
- STATUS: '/api/status',
- CONFIGS: '/api/configs',
- CONFIG: '/api/config',
- VALIDATE: '/api/validate',
- SAVE: '/api/save',
- LOGS: '/api/logs',
- RELOAD: '/api/reload',
- REBOOT: '/api/reboot'
+ STATUS: "/api/status",
+ CONFIGS: "/api/configs",
+ CONFIG: "/api/config",
+ VALIDATE: "/api/validate",
+ SAVE: "/api/save",
+ LOGS: "/api/logs",
+ RELOAD: "/api/reload",
+ REBOOT: "/api/reboot",
};
Object.freeze(ApiEndpoints);
export {
- EditorMode,
- PanelType,
- InterfaceState,
- ThemeMode,
- ValidationState,
- ApiEndpoints
+ EditorMode,
+ PanelType,
+ InterfaceState,
+ ThemeMode,
+ ValidationState,
+ ApiEndpoints,
};
diff --git a/static/interface-renderer.js b/static/interface-renderer.js
index 8c01696..5145cf3 100644
--- a/static/interface-renderer.js
+++ b/static/interface-renderer.js
@@ -1,162 +1,213 @@
/* jshint esversion: 2024, module: true */
-import { Utils } from './utils.js';
-import { InterfaceState } from './enums.js';
+import { Utils } from "./utils.js";
/**
* Static Interface Renderer for displaying network interfaces
* @class InterfaceRenderer
*/
class InterfaceRenderer {
- /**
- * Render interface tabs
- * @static
- * @param {Array} interfaces - Array of interface objects
- * @param {Object} currentInterface - Current interface object
- * @returns {string} HTML string
- */
- static renderInterfaceTabs(interfaces, currentInterface) {
- if (!interfaces.length) {
- return '<div class="no-interfaces">No network interfaces found</div>';
- }
-
- const tabsHTML = interfaces.map(iface => {
- const state = Utils.getInterfaceState(iface);
- const stateClass = Utils.getStateClass(state);
- const stateText = Utils.getStateText(iface);
- const isActive = iface === currentInterface;
-
- return `
- <button class="interface-tab ${isActive ? 'active' : ''}"
+ /**
+ * Render interface tabs
+ * @static
+ * @param {Array} interfaces - Array of interface objects
+ * @param {Object} currentInterface - Current interface object
+ * @returns {string} HTML string
+ */
+ static renderInterfaceTabs(interfaces, currentInterface) {
+ if (!interfaces.length) {
+ return '<div class="no-interfaces">No network interfaces found</div>';
+ }
+
+ const tabsHTML = interfaces
+ .map((iface) => {
+ const state = Utils.getInterfaceState(iface);
+ const stateClass = Utils.getStateClass(state);
+ const stateText = Utils.getStateText(iface);
+ const isActive = iface === currentInterface;
+
+ return `
+ <button class="interface-tab ${isActive ? "active" : ""}"
data-interface="${Utils.sanitizeHTML(iface.Name)}">
${Utils.sanitizeHTML(iface.Name)}
<span class="interface-state ${stateClass}">${Utils.sanitizeHTML(stateText)}</span>
</button>
`;
- }).join('');
-
- return `<div class="interface-tabs-container">${tabsHTML}</div>`;
- }
-
- /**
- * Show detailed interface information
- * @static
- * @param {Object} iface - Interface object
- * @returns {string} HTML string
- */
- static showInterfaceDetails(iface) {
- const details = [
- InterfaceRenderer.renderDetailRow('Link File', iface.LinkFile),
- InterfaceRenderer.renderDetailRow('Network File', iface.NetworkFile),
- InterfaceRenderer.renderDetailRow('State', iface.State, Utils.getStateClass(Utils.getInterfaceState(iface))),
- InterfaceRenderer.renderDetailRow('Online State', iface.OnlineState),
- InterfaceRenderer.renderDetailRow('Type', iface.Type),
- InterfaceRenderer.renderDetailRow('Path', iface.Path),
- InterfaceRenderer.renderDetailRow('Driver', iface.Driver),
- InterfaceRenderer.renderDetailRow('Vendor', iface.Vendor),
- InterfaceRenderer.renderDetailRow('Model', iface.Model),
- InterfaceRenderer.renderDetailRow('Hardware Address', Utils.arrayToMac(iface.HardwareAddress)),
- InterfaceRenderer.renderDetailRow('MTU', iface.MTU ? `${iface.MTU} (min: ${iface.MTUMin ?? '?'}, max: ${iface.MTUMax ?? '?'})` : ''),
- InterfaceRenderer.renderDetailRow('QDisc', iface.QDisc),
- InterfaceRenderer.renderDetailRow('IPv6 Address Generation Mode', iface.IPv6AddressGenerationMode),
- InterfaceRenderer.renderDetailRow('Number of Queues (Tx/Rx)', iface.Queues ? `${iface.Queues.Tx ?? '?'}/${iface.Queues.Rx ?? '?'}` : ''),
- InterfaceRenderer.renderDetailRow('Auto negotiation', iface.AutoNegotiation ? 'yes' : 'no'),
- InterfaceRenderer.renderDetailRow('Speed', iface.Speed),
- InterfaceRenderer.renderDetailRow('Duplex', iface.Duplex),
- InterfaceRenderer.renderDetailRow('Port', iface.Port),
- InterfaceRenderer.renderDetailRow('Address', InterfaceRenderer.renderAddressList(iface.Addresses)),
- InterfaceRenderer.renderDetailRow('DNS', InterfaceRenderer.renderDNSServerList(iface.DNS)),
- InterfaceRenderer.renderDetailRow('NTP', iface.NTP),
- InterfaceRenderer.renderDetailRow('Activation Policy', iface.ActivationPolicy),
- InterfaceRenderer.renderDetailRow('Required For Online', iface.RequiredForOnline ? 'yes' : 'no'),
- InterfaceRenderer.renderDetailRow('Connected To', iface.ConnectedTo),
- InterfaceRenderer.renderDetailRow('Offered DHCP leases', InterfaceRenderer.renderDHCPLeases(iface.DHCPLeases))
- ].filter(Boolean).join('');
-
- return `<div class="interface-detail-grid">${details}</div>`;
- }
-
- /**
- * Render a detail row with abbreviations
- * @static
- * @param {string} label - Row label
- * @param {string} value - Row value
- * @param {string} [valueClass] - CSS class for value
- * @returns {string} HTML string
- */
- static renderDetailRow(label, value, valueClass = '') {
- if (!value) return '';
-
- const abbreviations = {
- 'MTU': 'Maximum Transmission Unit',
- 'QDisc': 'Queueing Discipline',
- 'Tx': 'Transmit',
- 'Rx': 'Receive',
- 'DNS': 'Domain Name System',
- 'NTP': 'Network Time Protocol',
- 'DHCP': 'Dynamic Host Configuration Protocol',
- 'MAC': 'Media Access Control',
- 'IP': 'Internet Protocol',
- 'IPv6': 'Internet Protocol version 6'
- };
-
- const abbrLabel = Object.keys(abbreviations).includes(label)
- ? `<abbr title="${abbreviations[label]}">${label}</abbr>`
- : label;
-
- return `
+ })
+ .join("");
+
+ return `<div class="interface-tabs-container">${tabsHTML}</div>`;
+ }
+
+ /**
+ * Show detailed interface information
+ * @static
+ * @param {Object} iface - Interface object
+ * @returns {string} HTML string
+ */
+ static showInterfaceDetails(iface) {
+ const details = [
+ InterfaceRenderer.renderDetailRow("Link File", iface.LinkFile),
+ InterfaceRenderer.renderDetailRow("Network File", iface.NetworkFile),
+ InterfaceRenderer.renderDetailRow(
+ "State",
+ iface.State,
+ Utils.getStateClass(Utils.getInterfaceState(iface)),
+ ),
+ InterfaceRenderer.renderDetailRow("Online State", iface.OnlineState),
+ InterfaceRenderer.renderDetailRow("Type", iface.Type),
+ InterfaceRenderer.renderDetailRow("Path", iface.Path),
+ InterfaceRenderer.renderDetailRow("Driver", iface.Driver),
+ InterfaceRenderer.renderDetailRow("Vendor", iface.Vendor),
+ InterfaceRenderer.renderDetailRow("Model", iface.Model),
+ InterfaceRenderer.renderDetailRow(
+ "Hardware Address",
+ Utils.arrayToMac(iface.HardwareAddress),
+ ),
+ InterfaceRenderer.renderDetailRow(
+ "MTU",
+ iface.MTU
+ ? `${iface.MTU} (min: ${iface.MTUMin ?? "?"}, max: ${iface.MTUMax ?? "?"})`
+ : "",
+ ),
+ InterfaceRenderer.renderDetailRow("QDisc", iface.QDisc),
+ InterfaceRenderer.renderDetailRow(
+ "IPv6 Address Generation Mode",
+ iface.IPv6AddressGenerationMode,
+ ),
+ InterfaceRenderer.renderDetailRow(
+ "Number of Queues (Tx/Rx)",
+ iface.Queues
+ ? `${iface.Queues.Tx ?? "?"}/${iface.Queues.Rx ?? "?"}`
+ : "",
+ ),
+ InterfaceRenderer.renderDetailRow(
+ "Auto negotiation",
+ iface.AutoNegotiation ? "yes" : "no",
+ ),
+ InterfaceRenderer.renderDetailRow("Speed", iface.Speed),
+ InterfaceRenderer.renderDetailRow("Duplex", iface.Duplex),
+ InterfaceRenderer.renderDetailRow("Port", iface.Port),
+ InterfaceRenderer.renderDetailRow(
+ "Address",
+ InterfaceRenderer.renderAddressList(iface.Addresses),
+ ),
+ InterfaceRenderer.renderDetailRow(
+ "DNS",
+ InterfaceRenderer.renderDNSServerList(iface.DNS),
+ ),
+ InterfaceRenderer.renderDetailRow("NTP", iface.NTP),
+ InterfaceRenderer.renderDetailRow(
+ "Activation Policy",
+ iface.ActivationPolicy,
+ ),
+ InterfaceRenderer.renderDetailRow(
+ "Required For Online",
+ iface.RequiredForOnline ? "yes" : "no",
+ ),
+ InterfaceRenderer.renderDetailRow("Connected To", iface.ConnectedTo),
+ InterfaceRenderer.renderDetailRow(
+ "Offered DHCP leases",
+ InterfaceRenderer.renderDHCPLeases(iface.DHCPLeases),
+ ),
+ ]
+ .filter(Boolean)
+ .join("");
+
+ return `<div class="interface-detail-grid">${details}</div>`;
+ }
+
+ /**
+ * Render a detail row with abbreviations
+ * @static
+ * @param {string} label - Row label
+ * @param {string} value - Row value
+ * @param {string} [valueClass] - CSS class for value
+ * @returns {string} HTML string
+ */
+ static renderDetailRow(label, value, valueClass = "") {
+ if (!value) return "";
+
+ const abbreviations = {
+ MTU: "Maximum Transmission Unit",
+ QDisc: "Queueing Discipline",
+ Tx: "Transmit",
+ Rx: "Receive",
+ DNS: "Domain Name System",
+ NTP: "Network Time Protocol",
+ DHCP: "Dynamic Host Configuration Protocol",
+ MAC: "Media Access Control",
+ IP: "Internet Protocol",
+ IPv6: "Internet Protocol version 6",
+ };
+
+ const abbrLabel = Object.keys(abbreviations).includes(label)
+ ? `<abbr title="${abbreviations[label]}">${label}</abbr>`
+ : label;
+
+ return `
<div class="detail-row">
<span class="detail-label">${abbrLabel}:</span>
<span class="detail-value ${valueClass}">${Utils.sanitizeHTML(value)}</span>
</div>
`;
- }
-
- /**
- * Render address list
- * @static
- * @param {Array} addresses - Array of addresses
- * @returns {string} Formatted addresses
- */
- static renderAddressList(addresses) {
- if (!addresses?.length) return '';
-
- return addresses.map(addr => {
- const ip = Utils.ipFromArray(addr);
- return ip ? `<div class="address-item">${Utils.sanitizeHTML(ip)}</div>` : '';
- }).join('');
- }
-
- /**
- * Render DNS server list
- * @static
- * @param {Array} dnsServers - Array of DNS servers
- * @returns {string} Formatted DNS servers
- */
- static renderDNSServerList(dnsServers) {
- if (!dnsServers?.length) return '';
-
- return dnsServers.map(dns => {
- const server = Utils.ipFromArray(dns.Address ?? dns);
- return server ? `<div class="dns-item">${Utils.sanitizeHTML(server)}</div>` : '';
- }).join('');
- }
-
- /**
- * Render DHCP leases
- * @static
- * @param {Array} leases - Array of DHCP leases
- * @returns {string} Formatted leases
- */
- static renderDHCPLeases(leases) {
- if (!leases?.length) return '';
-
- return leases.map(lease => {
- const ip = lease.IP ?? lease;
- const to = lease.To ?? lease.MAC ?? '';
- return `<div class="lease-item">${Utils.sanitizeHTML(ip)} (to ${Utils.sanitizeHTML(to)})</div>`;
- }).join('');
- }
+ }
+
+ /**
+ * Render address list
+ * @static
+ * @param {Array} addresses - Array of addresses
+ * @returns {string} Formatted addresses
+ */
+ static renderAddressList(addresses) {
+ if (!addresses?.length) return "";
+
+ return addresses
+ .map((addr) => {
+ const ip = Utils.ipFromArray(addr);
+ return ip
+ ? `<div class="address-item">${Utils.sanitizeHTML(ip)}</div>`
+ : "";
+ })
+ .join("");
+ }
+
+ /**
+ * Render DNS server list
+ * @static
+ * @param {Array} dnsServers - Array of DNS servers
+ * @returns {string} Formatted DNS servers
+ */
+ static renderDNSServerList(dnsServers) {
+ if (!dnsServers?.length) return "";
+
+ return dnsServers
+ .map((dns) => {
+ const server = Utils.ipFromArray(dns.Address ?? dns);
+ return server
+ ? `<div class="dns-item">${Utils.sanitizeHTML(server)}</div>`
+ : "";
+ })
+ .join("");
+ }
+
+ /**
+ * Render DHCP leases
+ * @static
+ * @param {Array} leases - Array of DHCP leases
+ * @returns {string} Formatted leases
+ */
+ static renderDHCPLeases(leases) {
+ if (!leases?.length) return "";
+
+ return leases
+ .map((lease) => {
+ const ip = lease.IP ?? lease;
+ const to = lease.To ?? lease.MAC ?? "";
+ return `<div class="lease-item">${Utils.sanitizeHTML(ip)} (to ${Utils.sanitizeHTML(to)})</div>`;
+ })
+ .join("");
+ }
}
export { InterfaceRenderer };
diff --git a/static/network-types.js b/static/network-types.js
new file mode 100644
index 0000000..a37e7a6
--- /dev/null
+++ b/static/network-types.js
@@ -0,0 +1,180 @@
+/* jshint esversion: 2024, module: true */
+
+/**
+ * Network configuration types and enumerations
+ * @module NetworkTypes
+ */
+
+// Field types
+const FieldType = {
+ STRING: Symbol("string"),
+ MAC_ADDRESS: Symbol("mac-address"),
+ IPV4_ADDRESS: Symbol("ipv4-address"),
+ IPV6_ADDRESS: Symbol("ipv6-address"),
+ IP_PREFIX: Symbol("ip-prefix"),
+ BOOLEAN: Symbol("boolean"),
+ NUMBER: Symbol("number"),
+ PORT: Symbol("port"),
+ MTU: Symbol("mtu"),
+ STRINGS: Symbol("strings"),
+ IP_ADDRESSES: Symbol("ip-addresses"),
+ DHCP_MODE: Symbol("dhcp-mode"),
+ IP_FORWARD: Symbol("ip-forward"),
+ PRIVACY_EXTENSIONS: Symbol("privacy-extensions"),
+ LLMNR: Symbol("llmnr"),
+ MDNS: Symbol("mdns"),
+ DNSSEC: Symbol("dnssec"),
+ USE_DOMAINS: Symbol("use-domains"),
+ CLIENT_IDENTIFIER: Symbol("client-identifier"),
+ ROUTE_SCOPE: Symbol("route-scope"),
+ ROUTE_TYPE: Symbol("route-type"),
+ SLAAC: Symbol("slaac"),
+};
+Object.freeze(FieldType);
+
+// DHCP modes
+const DHCPMode = {
+ YES: "yes",
+ NO: "no",
+ IPV4: "ipv4",
+ IPV6: "ipv6",
+};
+Object.freeze(DHCPMode);
+
+// Boolean values
+const BooleanYesNo = {
+ YES: "yes",
+ NO: "no",
+};
+Object.freeze(BooleanYesNo);
+
+// IP forwarding options
+const IPForward = {
+ YES: "yes",
+ NO: "no",
+ IPV4: "ipv4",
+ IPV6: "ipv6",
+};
+Object.freeze(IPForward);
+
+// IPv6 privacy extensions
+const IPv6PrivacyExtensions = {
+ YES: "yes",
+ NO: "no",
+ PREFER_PUBLIC: "prefer-public",
+};
+Object.freeze(IPv6PrivacyExtensions);
+
+// LLMNR options
+const LLMNROptions = {
+ YES: "yes",
+ NO: "no",
+ RESOLVE: "resolve",
+};
+Object.freeze(LLMNROptions);
+
+// Multicast DNS options
+const MulticastDNS = {
+ YES: "yes",
+ NO: "no",
+ RESOLVE: "resolve",
+};
+Object.freeze(MulticastDNS);
+
+// DNSSEC options
+const DNSSECOptions = {
+ YES: "yes",
+ NO: "no",
+ ALLOW_DOWNGRADE: "allow-downgrade",
+};
+Object.freeze(DNSSECOptions);
+
+// Use domains options
+const UseDomains = {
+ YES: "yes",
+ NO: "no",
+ ROUTE: "route",
+};
+Object.freeze(UseDomains);
+
+// Client identifier options
+const ClientIdentifier = {
+ MAC: "mac",
+ DUID: "duid",
+};
+Object.freeze(ClientIdentifier);
+
+// Route scope options
+const RouteScope = {
+ GLOBAL: "global",
+ LINK: "link",
+ HOST: "host",
+};
+Object.freeze(RouteScope);
+
+// Route type options
+const RouteType = {
+ UNICAST: "unicast",
+ LOCAL: "local",
+ BROADCAST: "broadcast",
+ ANYCAST: "anycast",
+ MULTICAST: "multicast",
+ BLACKHOLE: "blackhole",
+ UNREACHABLE: "unreachable",
+ PROHIBIT: "prohibit",
+};
+Object.freeze(RouteType);
+
+// Wake-on-LAN options
+const WakeOnLAN = {
+ PHY: "phy",
+ UNICAST: "unicast",
+ BROADCAST: "broadcast",
+ ARP: "arp",
+ MAGIC: "magic",
+};
+Object.freeze(WakeOnLAN);
+
+// Port types
+const PortType = {
+ TP: "tp",
+ AUI: "aui",
+ BNC: "bnc",
+ MII: "mii",
+ FIBRE: "fibre",
+};
+Object.freeze(PortType);
+
+// Duplex modes
+const DuplexMode = {
+ HALF: "half",
+ FULL: "full",
+};
+Object.freeze(DuplexMode);
+
+// SLAAC options
+const SLAACOptions = {
+ YES: "yes",
+ NO: "no",
+ PREFER_TEMPORARY: "prefer-temporary",
+};
+Object.freeze(SLAACOptions);
+
+export {
+ FieldType,
+ DHCPMode,
+ BooleanYesNo,
+ IPForward,
+ IPv6PrivacyExtensions,
+ LLMNROptions,
+ MulticastDNS,
+ DNSSECOptions,
+ UseDomains,
+ ClientIdentifier,
+ RouteScope,
+ RouteType,
+ WakeOnLAN,
+ PortType,
+ DuplexMode,
+ SLAACOptions,
+};
diff --git a/static/structured-editor.js b/static/structured-editor.js
index c419024..e5bd45f 100644
--- a/static/structured-editor.js
+++ b/static/structured-editor.js
@@ -296,9 +296,7 @@ class StructuredEditor {
this.schema[section].items
) {
// Array section item
- if (
- this.schema[section].items[index]?.[key]
- ) {
+ if (this.schema[section].items[index]?.[key]) {
this.schema[section].items[index][key].value = value || null;
}
}
diff --git a/static/styles.css b/static/styles.css
index ca6468f..0092e12 100644
--- a/static/styles.css
+++ b/static/styles.css
@@ -1,915 +1,917 @@
:root {
- /* Light theme (default) */
- --color-neutral-background: #f7f7f7;
- --color-neutral-background-selected: #e6e6e6;
- --color-neutral-foreground: #242424;
- --color-neutral-foreground-subtle: #616161;
- --color-surface: #ffffff;
- --border-color: #e1e1e1;
- --shadow-color: rgba(0,0,0,0.12);
-
- /* Brand colors remain the same */
- --color-brand-background: #0078d4;
- --color-brand-background-hover: #106ebe;
- --color-brand-foreground: #0078d4;
- --color-brand-foreground-hover: #004578;
- --color-status-success: #107c10;
- --color-status-warning: #d83b01;
- --color-status-error: #d13438;
- --color-status-info: #0078d4;
-
- /* Shadows */
- --shadow-2: 0 0 2px var(--shadow-color), 0 2px 4px var(--shadow-color);
- --shadow-4: 0 0 2px var(--shadow-color), 0 4px 8px var(--shadow-color);
- --shadow-8: 0 0 2px var(--shadow-color), 0 8px 16px var(--shadow-color);
-
- /* Typography */
- --font-weight-regular: 400;
- --font-weight-semibold: 600;
- --font-weight-bold: 700;
-
- /* Spacing */
- --spacing-xs: 4px;
- --spacing-s: 8px;
- --spacing-m: 12px;
- --spacing-l: 16px;
- --spacing-xl: 20px;
- --spacing-xxl: 24px;
-
- /* Border Radius */
- --border-radius-medium: 4px;
- --border-radius-large: 8px;
+ /* Light theme (default) */
+ --color-neutral-background: #f7f7f7;
+ --color-neutral-background-selected: #e6e6e6;
+ --color-neutral-foreground: #242424;
+ --color-neutral-foreground-subtle: #616161;
+ --color-surface: #ffffff;
+ --border-color: #e1e1e1;
+ --shadow-color: rgba(0, 0, 0, 0.12);
+
+ /* Brand colors remain the same */
+ --color-brand-background: #0078d4;
+ --color-brand-background-hover: #106ebe;
+ --color-brand-foreground: #0078d4;
+ --color-brand-foreground-hover: #004578;
+ --color-status-success: #107c10;
+ --color-status-warning: #d83b01;
+ --color-status-error: #d13438;
+ --color-status-info: #0078d4;
+
+ /* Shadows */
+ --shadow-2: 0 0 2px var(--shadow-color), 0 2px 4px var(--shadow-color);
+ --shadow-4: 0 0 2px var(--shadow-color), 0 4px 8px var(--shadow-color);
+ --shadow-8: 0 0 2px var(--shadow-color), 0 8px 16px var(--shadow-color);
+
+ /* Typography */
+ --font-weight-regular: 400;
+ --font-weight-semibold: 600;
+ --font-weight-bold: 700;
+
+ /* Spacing */
+ --spacing-xs: 4px;
+ --spacing-s: 8px;
+ --spacing-m: 12px;
+ --spacing-l: 16px;
+ --spacing-xl: 20px;
+ --spacing-xxl: 24px;
+
+ /* Border Radius */
+ --border-radius-medium: 4px;
+ --border-radius-large: 8px;
}
[data-theme="dark"] {
- --color-neutral-background: #1f1f1f;
- --color-neutral-background-selected: #2d2d2d;
- --color-neutral-foreground: #ffffff;
- --color-neutral-foreground-subtle: #a0a0a0;
- --color-surface: #2d2d2d;
- --border-color: #404040;
- --shadow-color: rgba(0,0,0,0.3);
+ --color-neutral-background: #1f1f1f;
+ --color-neutral-background-selected: #2d2d2d;
+ --color-neutral-foreground: #ffffff;
+ --color-neutral-foreground-subtle: #a0a0a0;
+ --color-surface: #2d2d2d;
+ --border-color: #404040;
+ --shadow-color: rgba(0, 0, 0, 0.3);
}
/* Update all color references to use theme variables */
body {
- font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
- margin: 0;
- background-color: var(--color-neutral-background);
- color: var(--color-neutral-foreground);
- line-height: 1.4;
- transition: background-color 0.3s ease, color 0.3s ease;
+ font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
+ margin: 0;
+ background-color: var(--color-neutral-background);
+ color: var(--color-neutral-foreground);
+ line-height: 1.4;
+ transition:
+ background-color 0.3s ease,
+ color 0.3s ease;
}
header {
- background: var(--color-surface);
- padding: var(--spacing-l) var(--spacing-xl);
- border-bottom: 1px solid var(--border-color);
- box-shadow: var(--shadow-2);
+ background: var(--color-surface);
+ padding: var(--spacing-l) var(--spacing-xl);
+ border-bottom: 1px solid var(--border-color);
+ box-shadow: var(--shadow-2);
}
/* Add theme toggle button */
.theme-toggle {
- position: absolute;
- top: var(--spacing-l);
- right: var(--spacing-xl);
- background: none;
- border: none;
- font-size: 20px;
- cursor: pointer;
- padding: var(--spacing-s);
- border-radius: var(--border-radius-medium);
- transition: background-color 0.2s ease;
+ position: absolute;
+ top: var(--spacing-l);
+ right: var(--spacing-xl);
+ background: none;
+ border: none;
+ font-size: 20px;
+ cursor: pointer;
+ padding: var(--spacing-s);
+ border-radius: var(--border-radius-medium);
+ transition: background-color 0.2s ease;
}
.theme-toggle:hover {
- background-color: var(--color-neutral-background-selected);
+ background-color: var(--color-neutral-background-selected);
}
* {
- box-sizing: border-box;
+ box-sizing: border-box;
}
header h2 {
- margin: 0 0 var(--spacing-xs) 0;
- font-weight: var(--font-weight-semibold);
- font-size: 24px;
- color: var(--color-neutral-foreground);
+ margin: 0 0 var(--spacing-xs) 0;
+ font-weight: var(--font-weight-semibold);
+ font-size: 24px;
+ color: var(--color-neutral-foreground);
}
.subtitle {
- font-size: 14px;
- color: var(--color-neutral-foreground-subtle);
- margin: 0;
+ font-size: 14px;
+ color: var(--color-neutral-foreground-subtle);
+ margin: 0;
}
/* Main Layout */
main {
- display: flex;
- min-height: calc(100vh - 80px);
+ display: flex;
+ min-height: calc(100vh - 80px);
}
/* Navigation */
nav {
- width: 260px;
- background: var(--color-surface);
- border-right: 1px solid var(--border-color);
- padding: var(--spacing-l);
- display: flex;
- flex-direction: column;
- gap: var(--spacing-xs);
+ width: 260px;
+ background: var(--color-surface);
+ border-right: 1px solid var(--border-color);
+ padding: var(--spacing-l);
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-xs);
}
.nav-button {
- display: flex;
- align-items: center;
- gap: var(--spacing-s);
- padding: var(--spacing-m) var(--spacing-l);
- background: none;
- border: 1px solid transparent;
- border-radius: var(--border-radius-medium);
- font-family: inherit;
- font-size: 14px;
- font-weight: var(--font-weight-regular);
- color: var(--color-neutral-foreground);
- cursor: pointer;
- transition: all 0.1s ease;
- text-align: left;
- text-decoration: none;
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-s);
+ padding: var(--spacing-m) var(--spacing-l);
+ background: none;
+ border: 1px solid transparent;
+ border-radius: var(--border-radius-medium);
+ font-family: inherit;
+ font-size: 14px;
+ font-weight: var(--font-weight-regular);
+ color: var(--color-neutral-foreground);
+ cursor: pointer;
+ transition: all 0.1s ease;
+ text-align: left;
+ text-decoration: none;
}
.nav-button:hover {
- background-color: var(--color-neutral-background-selected);
- border-color: var(--border-color);
+ background-color: var(--color-neutral-background-selected);
+ border-color: var(--border-color);
}
.nav-button.active {
- background-color: var(--color-brand-background);
- color: white;
- font-weight: var(--font-weight-semibold);
- border-color: var(--color-brand-background);
+ background-color: var(--color-brand-background);
+ color: white;
+ font-weight: var(--font-weight-semibold);
+ border-color: var(--color-brand-background);
}
.nav-button.accent {
- background-color: var(--color-brand-background);
- color: white;
- font-weight: var(--font-weight-semibold);
- margin-top: var(--spacing-xl);
- border-color: var(--color-brand-background);
+ background-color: var(--color-brand-background);
+ color: white;
+ font-weight: var(--font-weight-semibold);
+ margin-top: var(--spacing-xl);
+ border-color: var(--color-brand-background);
}
.nav-button.accent:hover {
- background-color: var(--color-brand-background-hover);
- border-color: var(--color-brand-background-hover);
+ background-color: var(--color-brand-background-hover);
+ border-color: var(--color-brand-background-hover);
}
.nav-button.warning {
- background-color: var(--color-status-warning);
- color: white;
- border-color: var(--color-status-warning);
+ background-color: var(--color-status-warning);
+ color: white;
+ border-color: var(--color-status-warning);
}
.nav-button.warning:hover {
- background-color: #c13501;
- border-color: #c13501;
+ background-color: #c13501;
+ border-color: #c13501;
}
/* Tips Section */
.tips-section {
- margin-top: auto;
- padding-top: var(--spacing-xl);
- border-top: 1px solid var(--border-color);
+ margin-top: auto;
+ padding-top: var(--spacing-xl);
+ border-top: 1px solid var(--border-color);
}
.tips-title {
- font-weight: var(--font-weight-semibold);
- font-size: 14px;
- margin-bottom: var(--spacing-s);
- color: var(--color-neutral-foreground);
+ font-weight: var(--font-weight-semibold);
+ font-size: 14px;
+ margin-bottom: var(--spacing-s);
+ color: var(--color-neutral-foreground);
}
.tips-list {
- margin: 0;
- padding-left: var(--spacing-l);
- font-size: 13px;
- color: var(--color-neutral-foreground-subtle);
- line-height: 1.5;
+ margin: 0;
+ padding-left: var(--spacing-l);
+ font-size: 13px;
+ color: var(--color-neutral-foreground-subtle);
+ line-height: 1.5;
}
.tips-list li {
- margin-bottom: var(--spacing-xs);
+ margin-bottom: var(--spacing-xs);
}
/* Content Area */
section {
- flex: 1;
- padding: var(--spacing-xl);
- overflow: auto;
- display: none;
+ flex: 1;
+ padding: var(--spacing-xl);
+ overflow: auto;
+ display: none;
}
section.active {
- display: block;
+ display: block;
}
.card {
- background: var(--color-surface);
- border-radius: var(--border-radius-large);
- padding: var(--spacing-xl);
- margin-bottom: var(--spacing-l);
- box-shadow: var(--shadow-2);
- border: 1px solid var(--border-color);
+ background: var(--color-surface);
+ border-radius: var(--border-radius-large);
+ padding: var(--spacing-xl);
+ margin-bottom: var(--spacing-l);
+ box-shadow: var(--shadow-2);
+ border: 1px solid var(--border-color);
}
.card-header {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- margin-bottom: var(--spacing-l);
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: var(--spacing-l);
}
.card-title {
- margin: 0;
- font-weight: var(--font-weight-semibold);
- font-size: 20px;
- color: var(--color-neutral-foreground);
+ margin: 0;
+ font-weight: var(--font-weight-semibold);
+ font-size: 20px;
+ color: var(--color-neutral-foreground);
}
.card-description {
- margin: var(--spacing-xs) 0 0 0;
- font-size: 14px;
- color: var(--color-neutral-foreground-subtle);
+ margin: var(--spacing-xs) 0 0 0;
+ font-size: 14px;
+ color: var(--color-neutral-foreground-subtle);
}
/* Buttons */
.button {
- display: inline-flex;
- align-items: center;
- gap: var(--spacing-s);
- padding: var(--spacing-m) var(--spacing-l);
- background: var(--color-brand-background);
- color: white;
- border: none;
- border-radius: var(--border-radius-medium);
- font-family: inherit;
- font-size: 14px;
- font-weight: var(--font-weight-semibold);
- cursor: pointer;
- transition: background-color 0.1s ease;
- text-decoration: none;
+ display: inline-flex;
+ align-items: center;
+ gap: var(--spacing-s);
+ padding: var(--spacing-m) var(--spacing-l);
+ background: var(--color-brand-background);
+ color: white;
+ border: none;
+ border-radius: var(--border-radius-medium);
+ font-family: inherit;
+ font-size: 14px;
+ font-weight: var(--font-weight-semibold);
+ cursor: pointer;
+ transition: background-color 0.1s ease;
+ text-decoration: none;
}
.button:hover {
- background-color: var(--color-brand-background-hover);
+ background-color: var(--color-brand-background-hover);
}
.button.secondary {
- background: transparent;
- color: var(--color-brand-foreground);
- border: 1px solid var(--color-brand-foreground);
+ background: transparent;
+ color: var(--color-brand-foreground);
+ border: 1px solid var(--color-brand-foreground);
}
.button.secondary:hover {
- background-color: var(--color-neutral-background);
+ background-color: var(--color-neutral-background);
}
.button.warning {
- background: var(--color-status-warning);
+ background: var(--color-status-warning);
}
.button.warning:hover {
- background-color: #c13501;
+ background-color: #c13501;
}
/* Interface Grid */
.interface-grid {
- display: grid;
- gap: var(--spacing-l);
- margin-top: var(--spacing-l);
+ display: grid;
+ gap: var(--spacing-l);
+ margin-top: var(--spacing-l);
}
.interface-card {
- background: var(--color-surface);
- border-radius: var(--border-radius-large);
- padding: var(--spacing-xl);
- box-shadow: var(--shadow-2);
- border: 1px solid var(--border-color);
- border-left: 4px solid var(--color-brand-background);
+ background: var(--color-surface);
+ border-radius: var(--border-radius-large);
+ padding: var(--spacing-xl);
+ box-shadow: var(--shadow-2);
+ border: 1px solid var(--border-color);
+ border-left: 4px solid var(--color-brand-background);
}
.interface-card.wan {
- border-left-color: var(--color-status-warning);
+ border-left-color: var(--color-status-warning);
}
.interface-card.lan {
- border-left-color: var(--color-status-success);
+ border-left-color: var(--color-status-success);
}
.interface-header {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- margin-bottom: var(--spacing-l);
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: var(--spacing-l);
}
.interface-name {
- font-weight: var(--font-weight-semibold);
- font-size: 18px;
- margin: 0;
- color: var(--color-neutral-foreground);
+ font-weight: var(--font-weight-semibold);
+ font-size: 18px;
+ margin: 0;
+ color: var(--color-neutral-foreground);
}
.interface-type {
- display: inline-block;
- padding: var(--spacing-xs) var(--spacing-s);
- background: var(--color-neutral-background);
- border-radius: var(--border-radius-medium);
- font-size: 12px;
- font-weight: var(--font-weight-semibold);
- color: var(--color-neutral-foreground-subtle);
- margin-left: var(--spacing-s);
+ display: inline-block;
+ padding: var(--spacing-xs) var(--spacing-s);
+ background: var(--color-neutral-background);
+ border-radius: var(--border-radius-medium);
+ font-size: 12px;
+ font-weight: var(--font-weight-semibold);
+ color: var(--color-neutral-foreground-subtle);
+ margin-left: var(--spacing-s);
}
.interface-details {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: var(--spacing-l);
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: var(--spacing-l);
}
.detail-group h4 {
- margin: 0 0 var(--spacing-xs) 0;
- font-size: 14px;
- font-weight: var(--font-weight-semibold);
- color: var(--color-neutral-foreground);
+ margin: 0 0 var(--spacing-xs) 0;
+ font-size: 14px;
+ font-weight: var(--font-weight-semibold);
+ color: var(--color-neutral-foreground);
}
.detail-value {
- font-size: 14px;
- color: var(--color-neutral-foreground-subtle);
- line-height: 1.5;
+ font-size: 14px;
+ color: var(--color-neutral-foreground-subtle);
+ line-height: 1.5;
}
.state-badge {
- display: inline-block;
- padding: 2px 8px;
- border-radius: 10px;
- font-size: 12px;
- font-weight: var(--font-weight-semibold);
+ display: inline-block;
+ padding: 2px 8px;
+ border-radius: 10px;
+ font-size: 12px;
+ font-weight: var(--font-weight-semibold);
}
.state-up {
- background: #dff6dd;
- color: var(--color-status-success);
+ background: #dff6dd;
+ color: var(--color-status-success);
}
.state-down {
- background: #f4d5d5;
- color: var(--color-status-error);
+ background: #f4d5d5;
+ color: var(--color-status-error);
}
/* Forms */
/* Forms */
.form-group {
- margin-bottom: var(--spacing-l);
+ margin-bottom: var(--spacing-l);
}
.form-label {
- display: block;
- margin-bottom: var(--spacing-s);
- font-weight: var(--font-weight-semibold);
- font-size: 14px;
- color: var(--color-neutral-foreground);
+ display: block;
+ margin-bottom: var(--spacing-s);
+ font-weight: var(--font-weight-semibold);
+ font-size: 14px;
+ color: var(--color-neutral-foreground);
}
.select {
- width: 100%;
- padding: var(--spacing-m);
- border: 1px solid var(--border-color);
- border-radius: var(--border-radius-medium);
- font-family: inherit;
- font-size: 14px;
- background: var(--color-surface);
- color: var(--color-neutral-foreground);
- transition: border-color 0.1s ease;
+ width: 100%;
+ padding: var(--spacing-m);
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius-medium);
+ font-family: inherit;
+ font-size: 14px;
+ background: var(--color-surface);
+ color: var(--color-neutral-foreground);
+ transition: border-color 0.1s ease;
}
.select:focus {
- outline: none;
- border-color: var(--color-brand-background);
+ outline: none;
+ border-color: var(--color-brand-background);
}
.textarea {
- width: 100%;
- height: 360px;
- padding: var(--spacing-m);
- border: 1px solid var(--border-color);
- border-radius: var(--border-radius-medium);
- font-family: 'Consolas', 'Monaco', monospace;
- font-size: 13px;
- line-height: 1.5;
- resize: vertical;
- background: var(--color-surface);
- color: var(--color-neutral-foreground);
- transition: border-color 0.1s ease;
+ width: 100%;
+ height: 360px;
+ padding: var(--spacing-m);
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius-medium);
+ font-family: "Consolas", "Monaco", monospace;
+ font-size: 13px;
+ line-height: 1.5;
+ resize: vertical;
+ background: var(--color-surface);
+ color: var(--color-neutral-foreground);
+ transition: border-color 0.1s ease;
}
.textarea:focus {
- outline: none;
- border-color: var(--color-brand-background);
+ outline: none;
+ border-color: var(--color-brand-background);
}
.checkbox-group {
- display: flex;
- align-items: center;
- gap: var(--spacing-s);
- margin: var(--spacing-l) 0;
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-s);
+ margin: var(--spacing-l) 0;
}
.checkbox {
- width: 16px;
- height: 16px;
- margin: 0;
+ width: 16px;
+ height: 16px;
+ margin: 0;
}
.checkbox-label {
- font-size: 14px;
- color: var(--color-neutral-foreground);
- margin: 0;
+ font-size: 14px;
+ color: var(--color-neutral-foreground);
+ margin: 0;
}
/* Validation Results */
.validation-result {
- padding: var(--spacing-m);
- border-radius: var(--border-radius-medium);
- font-size: 14px;
- margin-top: var(--spacing-m);
+ padding: var(--spacing-m);
+ border-radius: var(--border-radius-medium);
+ font-size: 14px;
+ margin-top: var(--spacing-m);
}
.validation-success {
- background: #dff6dd;
- color: var(--color-status-success);
- border: 1px solid #107c10;
+ background: #dff6dd;
+ color: var(--color-status-success);
+ border: 1px solid #107c10;
}
.validation-error {
- background: #f4d5d5;
- color: var(--color-status-error);
- border: 1px solid var(--color-status-error);
+ background: #f4d5d5;
+ color: var(--color-status-error);
+ border: 1px solid var(--color-status-error);
}
/* Logs */
.logs-container {
- background: #1e1e1e;
- color: #d4d4d4;
- padding: var(--spacing-l);
- border-radius: var(--border-radius-medium);
- font-family: 'Consolas', 'Monaco', monospace;
- font-size: 13px;
- line-height: 1.5;
- overflow: auto;
- max-height: 600px;
- white-space: pre-wrap;
+ background: #1e1e1e;
+ color: #d4d4d4;
+ padding: var(--spacing-l);
+ border-radius: var(--border-radius-medium);
+ font-family: "Consolas", "Monaco", monospace;
+ font-size: 13px;
+ line-height: 1.5;
+ overflow: auto;
+ max-height: 600px;
+ white-space: pre-wrap;
}
/* Command Results */
.command-result {
- padding: var(--spacing-m);
- border-radius: var(--border-radius-medium);
- background: var(--color-neutral-background);
- font-size: 14px;
- margin-top: var(--spacing-m);
+ padding: var(--spacing-m);
+ border-radius: var(--border-radius-medium);
+ background: var(--color-neutral-background);
+ font-size: 14px;
+ margin-top: var(--spacing-m);
}
/* Tips Section */
.tips-section {
- margin-top: auto;
- padding-top: var(--spacing-xl);
- border-top: 1px solid #e1e1e1;
+ margin-top: auto;
+ padding-top: var(--spacing-xl);
+ border-top: 1px solid #e1e1e1;
}
.tips-title {
- font-weight: var(--font-weight-semibold);
- font-size: 14px;
- margin-bottom: var(--spacing-s);
- color: var(--color-neutral-foreground);
+ font-weight: var(--font-weight-semibold);
+ font-size: 14px;
+ margin-bottom: var(--spacing-s);
+ color: var(--color-neutral-foreground);
}
.tips-list {
- margin: 0;
- padding-left: var(--spacing-l);
- font-size: 13px;
- color: var(--color-neutral-foreground-subtle);
- line-height: 1.5;
+ margin: 0;
+ padding-left: var(--spacing-l);
+ font-size: 13px;
+ color: var(--color-neutral-foreground-subtle);
+ line-height: 1.5;
}
.tips-list li {
- margin-bottom: var(--spacing-xs);
+ margin-bottom: var(--spacing-xs);
}
/* Responsive */
@media (max-width: 768px) {
- main {
- flex-direction: column;
- }
+ main {
+ flex-direction: column;
+ }
- nav {
- width: 100%;
- flex-direction: row;
- overflow-x: auto;
- padding: var(--spacing-m);
- }
+ nav {
+ width: 100%;
+ flex-direction: row;
+ overflow-x: auto;
+ padding: var(--spacing-m);
+ }
- .nav-button {
- white-space: nowrap;
- }
+ .nav-button {
+ white-space: nowrap;
+ }
- .interface-details {
- grid-template-columns: 1fr;
- }
+ .interface-details {
+ grid-template-columns: 1fr;
+ }
- .card-header {
- flex-direction: column;
- gap: var(--spacing-m);
- }
+ .card-header {
+ flex-direction: column;
+ gap: var(--spacing-m);
+ }
}
/* Interface Tabs */
/* Interface Tabs */
.interface-tabs-container {
- display: flex;
- gap: var(--spacing-xs);
- margin-bottom: var(--spacing-l);
- flex-wrap: wrap;
+ display: flex;
+ gap: var(--spacing-xs);
+ margin-bottom: var(--spacing-l);
+ flex-wrap: wrap;
}
.interface-tab {
- display: flex;
- align-items: center;
- gap: var(--spacing-s);
- padding: var(--spacing-m) var(--spacing-l);
- background: var(--color-neutral-background);
- border: 1px solid var(--border-color);
- border-radius: var(--border-radius-medium);
- font-family: inherit;
- font-size: 14px;
- font-weight: var(--font-weight-regular);
- color: var(--color-neutral-foreground);
- cursor: pointer;
- transition: all 0.1s ease;
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-s);
+ padding: var(--spacing-m) var(--spacing-l);
+ background: var(--color-neutral-background);
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius-medium);
+ font-family: inherit;
+ font-size: 14px;
+ font-weight: var(--font-weight-regular);
+ color: var(--color-neutral-foreground);
+ cursor: pointer;
+ transition: all 0.1s ease;
}
.interface-tab:hover {
- background: var(--color-neutral-background-selected);
- border-color: var(--color-brand-background);
+ background: var(--color-neutral-background-selected);
+ border-color: var(--color-brand-background);
}
.interface-tab.active {
- background: var(--color-brand-background);
- color: white;
- border-color: var(--color-brand-background);
+ background: var(--color-brand-background);
+ color: white;
+ border-color: var(--color-brand-background);
}
.interface-state {
- font-size: 12px;
- padding: 2px 6px;
- border-radius: 10px;
- background: var(--color-neutral-foreground-subtle);
- color: white;
+ font-size: 12px;
+ padding: 2px 6px;
+ border-radius: 10px;
+ background: var(--color-neutral-foreground-subtle);
+ color: white;
}
.interface-tab.active .interface-state {
- background: rgba(255, 255, 255, 0.3);
+ background: rgba(255, 255, 255, 0.3);
}
/* Interface Details */
.interface-detail-grid {
- display: flex;
- flex-direction: column;
- gap: var(--spacing-s);
- font-family: 'Monaco', 'Consolas', monospace;
- font-size: 13px;
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-s);
+ font-family: "Monaco", "Consolas", monospace;
+ font-size: 13px;
}
.detail-row {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- gap: var(--spacing-l);
- padding: var(--spacing-xs) 0;
- border-bottom: 1px solid #f0f0f0;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: var(--spacing-l);
+ padding: var(--spacing-xs) 0;
+ border-bottom: 1px solid #f0f0f0;
}
.detail-row:last-child {
- border-bottom: none;
+ border-bottom: none;
}
.detail-label {
- font-weight: var(--font-weight-semibold);
- color: var(--color-neutral-foreground);
- min-width: 200px;
- flex-shrink: 0;
+ font-weight: var(--font-weight-semibold);
+ color: var(--color-neutral-foreground);
+ min-width: 200px;
+ flex-shrink: 0;
}
.detail-value {
- color: var(--color-neutral-foreground-subtle);
- text-align: right;
- flex: 1;
- word-break: break-word;
+ color: var(--color-neutral-foreground-subtle);
+ text-align: right;
+ flex: 1;
+ word-break: break-word;
}
/* Address and list items */
.address-item,
.dns-item,
.lease-item {
- margin-bottom: 2px;
- padding: 1px 0;
+ margin-bottom: 2px;
+ padding: 1px 0;
}
.address-item:last-child,
.dns-item:last-child,
.lease-item:last-child {
- margin-bottom: 0;
+ margin-bottom: 0;
}
/* State badges in details */
.detail-value.state-up {
- color: var(--color-status-success);
- font-weight: var(--font-weight-semibold);
+ color: var(--color-status-success);
+ font-weight: var(--font-weight-semibold);
}
.detail-value.state-down {
- color: var(--color-status-error);
- font-weight: var(--font-weight-semibold);
+ color: var(--color-status-error);
+ font-weight: var(--font-weight-semibold);
}
/* Error and empty states */
.error-message {
- padding: var(--spacing-l);
- background: var(--color-status-error);
- color: white;
- border-radius: var(--border-radius-medium);
- text-align: center;
+ padding: var(--spacing-l);
+ background: var(--color-status-error);
+ color: white;
+ border-radius: var(--border-radius-medium);
+ text-align: center;
}
.no-interfaces {
- padding: var(--spacing-xl);
- text-align: center;
- color: var(--color-neutral-foreground-subtle);
- font-style: italic;
+ padding: var(--spacing-xl);
+ text-align: center;
+ color: var(--color-neutral-foreground-subtle);
+ font-style: italic;
}
/* Validation states */
.validation-pending {
- color: var(--color-neutral-foreground-subtle);
+ color: var(--color-neutral-foreground-subtle);
}
.validation-success {
- color: var(--color-status-success);
- font-weight: var(--font-weight-semibold);
+ color: var(--color-status-success);
+ font-weight: var(--font-weight-semibold);
}
.validation-error {
- color: var(--color-status-error);
- font-weight: var(--font-weight-semibold);
+ color: var(--color-status-error);
+ font-weight: var(--font-weight-semibold);
}
.validation-result {
- padding: var(--spacing-m);
- border-radius: var(--border-radius-medium);
- font-size: 14px;
- margin-top: var(--spacing-m);
+ padding: var(--spacing-m);
+ border-radius: var(--border-radius-medium);
+ font-size: 14px;
+ margin-top: var(--spacing-m);
}
.validation-success {
- background: var(--color-status-success);
- color: white;
- border: 1px solid var(--color-status-success);
+ background: var(--color-status-success);
+ color: white;
+ border: 1px solid var(--color-status-success);
}
.validation-error {
- background: var(--color-status-error);
- color: white;
- border: 1px solid var(--color-status-error);
+ background: var(--color-status-error);
+ color: white;
+ border: 1px solid var(--color-status-error);
}
/* Responsive improvements */
@media (max-width: 768px) {
- .detail-row {
- flex-direction: column;
- gap: var(--spacing-xs);
- align-items: stretch;
- }
+ .detail-row {
+ flex-direction: column;
+ gap: var(--spacing-xs);
+ align-items: stretch;
+ }
- .detail-label {
- min-width: auto;
- font-size: 12px;
- }
+ .detail-label {
+ min-width: auto;
+ font-size: 12px;
+ }
- .detail-value {
- text-align: left;
- }
+ .detail-value {
+ text-align: left;
+ }
- .interface-tabs-container {
- flex-direction: column;
- }
+ .interface-tabs-container {
+ flex-direction: column;
+ }
- .interface-tab {
- justify-content: space-between;
- }
+ .interface-tab {
+ justify-content: space-between;
+ }
}
.interface-detail-grid {
- display: grid;
- grid-template-columns: auto 1fr;
- gap: var(--spacing-s) var(--spacing-l);
- font-family: 'Monaco', 'Consolas', monospace;
- font-size: 13px;
- align-items: start;
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: var(--spacing-s) var(--spacing-l);
+ font-family: "Monaco", "Consolas", monospace;
+ font-size: 13px;
+ align-items: start;
}
.detail-row {
- display: contents;
+ display: contents;
}
.detail-label {
- font-weight: var(--font-weight-semibold);
- color: var(--color-neutral-foreground);
- text-align: right;
- padding: var(--spacing-xs) 0;
- border-bottom: 1px solid var(--border-color);
+ font-weight: var(--font-weight-semibold);
+ color: var(--color-neutral-foreground);
+ text-align: right;
+ padding: var(--spacing-xs) 0;
+ border-bottom: 1px solid var(--border-color);
}
.detail-value {
- color: var(--color-neutral-foreground-subtle);
- text-align: left;
- padding: var(--spacing-xs) 0;
- border-bottom: 1px solid var(--border-color);
- word-break: break-word;
+ color: var(--color-neutral-foreground-subtle);
+ text-align: left;
+ padding: var(--spacing-xs) 0;
+ border-bottom: 1px solid var(--border-color);
+ word-break: break-word;
}
.detail-row:last-child .detail-label,
.detail-row:last-child .detail-value {
- border-bottom: none;
+ border-bottom: none;
}
/* Abbreviation styling */
abbr {
- text-decoration: underline dotted;
- cursor: help;
+ text-decoration: underline dotted;
+ cursor: help;
}
/* Address and list items */
.address-item,
.dns-item,
.lease-item {
- margin-bottom: 2px;
- padding: 1px 0;
+ margin-bottom: 2px;
+ padding: 1px 0;
}
.address-item:last-child,
.dns-item:last-child,
.lease-item:last-child {
- margin-bottom: 0;
+ margin-bottom: 0;
}
/* Structured Editor Styles */
.structured-editor {
- margin-top: var(--spacing-l);
+ margin-top: var(--spacing-l);
}
.editor-sections {
- display: flex;
- flex-direction: column;
- gap: var(--spacing-l);
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-l);
}
/* Editor Mode Toggle */
.editor-mode-toggle {
- display: flex;
- gap: var(--spacing-s);
- margin-bottom: var(--spacing-l);
- padding-bottom: var(--spacing-l);
- border-bottom: 1px solid var(--border-color);
+ display: flex;
+ gap: var(--spacing-s);
+ margin-bottom: var(--spacing-l);
+ padding-bottom: var(--spacing-l);
+ border-bottom: 1px solid var(--border-color);
}
.editor-mode-toggle .button.small {
- flex: 1;
- padding: var(--spacing-m) var(--spacing-l);
+ flex: 1;
+ padding: var(--spacing-m) var(--spacing-l);
}
.editor-mode-toggle .button.small.active {
- background-color: var(--color-brand-background);
- color: white;
- border-color: var(--color-brand-background);
+ background-color: var(--color-brand-background);
+ color: white;
+ border-color: var(--color-brand-background);
}
.config-actions {
- display: flex;
- gap: var(--spacing-s);
- align-items: center;
- margin-top: var(--spacing-l);
- padding-top: var(--spacing-l);
- border-top: 1px solid var(--border-color);
+ display: flex;
+ gap: var(--spacing-s);
+ align-items: center;
+ margin-top: var(--spacing-l);
+ padding-top: var(--spacing-l);
+ border-top: 1px solid var(--border-color);
}
.config-section {
- background: var(--color-surface);
- border: 1px solid var(--border-color);
- border-radius: var(--border-radius-medium);
- padding: var(--spacing-l);
+ background: var(--color-surface);
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius-medium);
+ padding: var(--spacing-l);
}
.config-section h4 {
- margin: 0 0 var(--spacing-m) 0;
- color: var(--color-brand-foreground);
- font-family: monospace;
+ margin: 0 0 var(--spacing-m) 0;
+ color: var(--color-brand-foreground);
+ font-family: monospace;
}
.config-table {
- display: grid;
- grid-template-columns: auto 1fr;
- gap: var(--spacing-s) var(--spacing-m);
- align-items: center;
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: var(--spacing-s) var(--spacing-m);
+ align-items: center;
}
.config-row {
- display: contents;
+ display: contents;
}
.config-label {
- text-align: right;
- font-weight: var(--font-weight-semibold);
- font-size: 13px;
- color: var(--color-neutral-foreground);
+ text-align: right;
+ font-weight: var(--font-weight-semibold);
+ font-size: 13px;
+ color: var(--color-neutral-foreground);
}
.config-input,
.config-select {
- padding: var(--spacing-s);
- border: 1px solid var(--border-color);
- border-radius: var(--border-radius-medium);
- background: var(--color-surface);
- color: var(--color-neutral-foreground);
- font-family: inherit;
- font-size: 13px;
+ padding: var(--spacing-s);
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius-medium);
+ background: var(--color-surface);
+ color: var(--color-neutral-foreground);
+ font-family: inherit;
+ font-size: 13px;
}
.config-input:focus,
.config-select:focus {
- outline: none;
- border-color: var(--color-brand-background);
+ outline: none;
+ border-color: var(--color-brand-background);
}
.button.small {
- padding: var(--spacing-xs) var(--spacing-s);
- font-size: 12px;
- grid-column: 1 / -1;
- justify-self: end;
- margin-top: var(--spacing-s);
+ padding: var(--spacing-xs) var(--spacing-s);
+ font-size: 12px;
+ grid-column: 1 / -1;
+ justify-self: end;
+ margin-top: var(--spacing-s);
}
.editor-actions {
- display: flex;
- gap: var(--spacing-s);
- margin-top: var(--spacing-l);
- padding-top: var(--spacing-l);
- border-top: 1px solid var(--border-color);
+ display: flex;
+ gap: var(--spacing-s);
+ margin-top: var(--spacing-l);
+ padding-top: var(--spacing-l);
+ border-top: 1px solid var(--border-color);
}
/* Structured Editor Enhancements */
.no-items {
- color: var(--color-neutral-foreground-subtle);
- font-style: italic;
- text-align: center;
- padding: var(--spacing-m);
+ color: var(--color-neutral-foreground-subtle);
+ font-style: italic;
+ text-align: center;
+ padding: var(--spacing-m);
}
.config-section {
- background: var(--color-surface);
- border: 1px solid var(--border-color);
- border-radius: var(--border-radius-medium);
- padding: var(--spacing-l);
- transition: border-color 0.2s ease;
+ background: var(--color-surface);
+ border: 1px solid var(--border-color);
+ border-radius: var(--border-radius-medium);
+ padding: var(--spacing-l);
+ transition: border-color 0.2s ease;
}
.config-section:hover {
- border-color: var(--color-brand-background);
+ border-color: var(--color-brand-background);
}
.config-section h4 {
- margin: 0 0 var(--spacing-m) 0;
- color: var(--color-brand-foreground);
- font-family: monospace;
- border-bottom: 1px solid var(--border-color);
- padding-bottom: var(--spacing-s);
+ margin: 0 0 var(--spacing-m) 0;
+ color: var(--color-brand-foreground);
+ font-family: monospace;
+ border-bottom: 1px solid var(--border-color);
+ padding-bottom: var(--spacing-s);
}
.button.warning {
- background: var(--color-status-warning);
- border-color: var(--color-status-warning);
+ background: var(--color-status-warning);
+ border-color: var(--color-status-warning);
}
.button.warning:hover {
- background: #c13501;
- border-color: #c13501;
+ background: #c13501;
+ border-color: #c13501;
}
diff --git a/static/systemd-network.js b/static/systemd-network.js
index 0c57ec1..69020f1 100644
--- a/static/systemd-network.js
+++ b/static/systemd-network.js
@@ -1,54 +1,132 @@
/* jshint esversion: 2024, module: true */
-/**
- * Systemd Network Configuration Parser
- * Based on systemd.network(5) documentation
- * @module SystemdNetwork
- */
+import {
+ BooleanYesNo,
+ ClientIdentifier,
+ DHCPMode,
+ DNSSECOptions,
+ DuplexMode,
+ FieldType,
+ IPForward,
+ IPv6PrivacyExtensions,
+ LLMNROptions,
+ MulticastDNS,
+ PortType,
+ RouteScope,
+ RouteType,
+ UseDomains,
+ WakeOnLAN,
+} from "./network-types.js";
/**
* Base field type with standardized interface
* @class BaseField
*/
class BaseField {
- /**
- * @param {*} value - Field value
- * @param {string} type - Field type
- * @param {string} description - Field description
- * @param {Object} options - Additional options (pattern, enum, etc.)
- */
- constructor(value = null, type = 'string', description = '', options = {}) {
- this.value = value;
- this.type = type;
- this.description = description;
- this.options = options;
- }
-
- /**
- * Validate field value
- * @returns {boolean}
- */
- validate() {
- if (this.value === null || this.value === undefined) return true;
-
- if (this.options.pattern && typeof this.value === 'string') {
- return this.options.pattern.test(this.value);
- }
-
- if (this.options.enum && this.options.enum.length > 0) {
- return this.options.enum.includes(this.value);
- }
-
- return true;
- }
-
- /**
- * Convert to string representation
- * @returns {string}
- */
- toString() {
- return this.value !== null ? String(this.value) : '';
- }
+ #value;
+ #type;
+ #description;
+ #options;
+
+ /**
+ * @param {*} value - Field value
+ * @param {Symbol} type - Field type from FieldType enum
+ * @param {string} description - Field description
+ * @param {Object} options - Additional options (pattern, enum, etc.)
+ */
+ constructor(
+ value = null,
+ type = FieldType.STRING,
+ description = "",
+ options = {},
+ ) {
+ this.#value = value;
+ this.#type = type;
+ this.#description = description;
+ this.#options = options;
+ }
+
+ /**
+ * Get field value
+ * @returns {*}
+ */
+ get value() {
+ return this.#value;
+ }
+
+ /**
+ * Set field value
+ * @param {*} newValue
+ */
+ set value(newValue) {
+ this.#value = newValue;
+ }
+
+ /**
+ * Get field type
+ * @returns {Symbol}
+ */
+ get type() {
+ return this.#type;
+ }
+
+ /**
+ * Get field description
+ * @returns {string}
+ */
+ get description() {
+ return this.#description;
+ }
+
+ /**
+ * Get field options
+ * @returns {Object}
+ */
+ get options() {
+ return { ...this.#options };
+ }
+
+ /**
+ * Validate field value
+ * @returns {boolean}
+ */
+ validate() {
+ if (
+ this.#value === null ||
+ this.#value === undefined ||
+ this.#value === ""
+ ) {
+ return true;
+ }
+
+ if (this.#options.pattern && typeof this.#value === "string") {
+ return this.#options.pattern.test(this.#value);
+ }
+
+ if (this.#options.enum && this.#options.enum.length > 0) {
+ return this.#options.enum.includes(this.#value);
+ }
+
+ return true;
+ }
+
+ /**
+ * Convert to string representation
+ * @returns {string}
+ */
+ toString() {
+ return this.#value !== null ? String(this.#value) : "";
+ }
+
+ /**
+ * Check if field has a value
+ * @returns {boolean}
+ */
+ hasValue() {
+ return (
+ this.#value !== null && this.#value !== undefined && this.#value !== ""
+ );
+ }
}
/**
@@ -56,11 +134,11 @@ class BaseField {
* @class MACAddress
*/
class MACAddress extends BaseField {
- constructor(value = null) {
- super(value, 'mac-address', 'Hardware address (MAC)', {
- pattern: /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/
- });
- }
+ constructor(value = null) {
+ super(value, FieldType.MAC_ADDRESS, "Hardware address (MAC)", {
+ pattern: /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/,
+ });
+ }
}
/**
@@ -68,11 +146,11 @@ class MACAddress extends BaseField {
* @class IPv4Address
*/
class IPv4Address extends BaseField {
- constructor(value = null) {
- super(value, 'ipv4-address', 'IPv4 address', {
- pattern: /^(\d{1,3}\.){3}\d{1,3}$/
- });
- }
+ constructor(value = null) {
+ super(value, FieldType.IPV4_ADDRESS, "IPv4 address", {
+ pattern: /^(\d{1,3}\.){3}\d{1,3}$/,
+ });
+ }
}
/**
@@ -80,23 +158,11 @@ class IPv4Address extends BaseField {
* @class IPv6Address
*/
class IPv6Address extends BaseField {
- constructor(value = null) {
- super(value, 'ipv6-address', 'IPv6 address', {
- pattern: /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/
- });
- }
-}
-
-/**
- * Boolean type with yes/no values
- * @class BooleanYesNo
- */
-class BooleanYesNo extends BaseField {
- constructor(value = null) {
- super(value, 'boolean', 'Boolean (yes/no)', {
- enum: ['yes', 'no']
- });
- }
+ constructor(value = null) {
+ super(value, FieldType.IPV6_ADDRESS, "IPv6 address", {
+ pattern: /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/,
+ });
+ }
}
/**
@@ -104,11 +170,12 @@ class BooleanYesNo extends BaseField {
* @class Port
*/
class Port extends BaseField {
- constructor(value = null) {
- super(value, 'port', 'Network port (1-65535)', {
- pattern: /^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/
- });
- }
+ constructor(value = null) {
+ super(value, FieldType.PORT, "Network port (1-65535)", {
+ pattern:
+ /^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/,
+ });
+ }
}
/**
@@ -116,11 +183,11 @@ class Port extends BaseField {
* @class MTU
*/
class MTU extends BaseField {
- constructor(value = null) {
- super(value, 'mtu', 'Maximum Transmission Unit', {
- pattern: /^[1-9][0-9]*$/
- });
- }
+ constructor(value = null) {
+ super(value, FieldType.MTU, "Maximum Transmission Unit", {
+ pattern: /^[1-9][0-9]*$/,
+ });
+ }
}
/**
@@ -128,19 +195,43 @@ class MTU extends BaseField {
* @class MatchSection
*/
class MatchSection {
- constructor() {
- this.MACAddress = new BaseField(null, 'mac-addresses', 'Space-separated MAC addresses');
- this.OriginalName = new BaseField(null, 'strings', 'Original interface names');
- this.Path = new BaseField(null, 'strings', 'Device path patterns');
- this.Driver = new BaseField(null, 'strings', 'Driver names');
- this.Type = new BaseField(null, 'strings', 'Interface types (ether, wifi, etc.)');
- this.Name = new BaseField(null, 'strings', 'Interface names');
- this.Property = new BaseField(null, 'string', 'Device property');
- this.Host = new BaseField(null, 'string', 'Host name');
- this.Virtualization = new BaseField(null, 'string', 'Virtualization detection');
- this.KernelCommandLine = new BaseField(null, 'string', 'Kernel command line');
- this.Architecture = new BaseField(null, 'string', 'System architecture');
- }
+ constructor() {
+ this.MACAddress = new BaseField(
+ null,
+ FieldType.STRINGS,
+ "Space-separated MAC addresses",
+ );
+ this.OriginalName = new BaseField(
+ null,
+ FieldType.STRINGS,
+ "Original interface names",
+ );
+ this.Path = new BaseField(null, FieldType.STRINGS, "Device path patterns");
+ this.Driver = new BaseField(null, FieldType.STRINGS, "Driver names");
+ this.Type = new BaseField(
+ null,
+ FieldType.STRINGS,
+ "Interface types (ether, wifi, etc.)",
+ );
+ this.Name = new BaseField(null, FieldType.STRINGS, "Interface names");
+ this.Property = new BaseField(null, FieldType.STRING, "Device property");
+ this.Host = new BaseField(null, FieldType.STRING, "Host name");
+ this.Virtualization = new BaseField(
+ null,
+ FieldType.STRING,
+ "Virtualization detection",
+ );
+ this.KernelCommandLine = new BaseField(
+ null,
+ FieldType.STRING,
+ "Kernel command line",
+ );
+ this.Architecture = new BaseField(
+ null,
+ FieldType.STRING,
+ "System architecture",
+ );
+ }
}
/**
@@ -148,24 +239,32 @@ class MatchSection {
* @class LinkSection
*/
class LinkSection {
- constructor() {
- this.MACAddress = new MACAddress();
- this.MTUBytes = new MTU();
- this.BitsPerSecond = new BaseField(null, 'number', 'Link speed in bits per second');
- this.Duplex = new BaseField(null, 'string', 'Duplex mode', {
- enum: ['half', 'full']
- });
- this.AutoNegotiation = new BooleanYesNo();
- this.WakeOnLan = new BaseField(null, 'string', 'Wake-on-LAN', {
- enum: ['phy', 'unicast', 'broadcast', 'arp', 'magic', '']
- });
- this.Port = new BaseField(null, 'string', 'Port type', {
- enum: ['tp', 'aui', 'bnc', 'mii', 'fibre', '']
- });
- this.Advertise = new BaseField(null, 'strings', 'Advertised features');
- this.RxFlowControl = new BooleanYesNo();
- this.TxFlowControl = new BooleanYesNo();
- }
+ constructor() {
+ this.MACAddress = new MACAddress();
+ this.MTUBytes = new MTU();
+ this.BitsPerSecond = new BaseField(
+ null,
+ FieldType.NUMBER,
+ "Link speed in bits per second",
+ );
+ this.Duplex = new BaseField(null, FieldType.STRING, "Duplex mode", {
+ enum: Object.values(DuplexMode),
+ });
+ this.AutoNegotiation = new BooleanYesNo();
+ this.WakeOnLan = new BaseField(null, FieldType.STRING, "Wake-on-LAN", {
+ enum: Object.values(WakeOnLAN),
+ });
+ this.Port = new BaseField(null, FieldType.STRING, "Port type", {
+ enum: Object.values(PortType),
+ });
+ this.Advertise = new BaseField(
+ null,
+ FieldType.STRINGS,
+ "Advertised features",
+ );
+ this.RxFlowControl = new BooleanYesNo();
+ this.TxFlowControl = new BooleanYesNo();
+ }
}
/**
@@ -173,35 +272,55 @@ class LinkSection {
* @class NetworkSection
*/
class NetworkSection {
- constructor() {
- this.Description = new BaseField(null, 'string', 'Interface description');
- this.DHCP = new BaseField(null, 'dhcp-mode', 'DHCP client', {
- enum: ['yes', 'no', 'ipv4', 'ipv6']
- });
- this.DHCPServer = new BooleanYesNo();
- this.DNS = new BaseField(null, 'ip-addresses', 'DNS servers');
- this.NTP = new BaseField(null, 'ip-addresses', 'NTP servers');
- this.IPForward = new BaseField(null, 'ip-forward', 'IP forwarding', {
- enum: ['yes', 'no', 'ipv4', 'ipv6']
- });
- this.IPv6PrivacyExtensions = new BaseField(null, 'privacy-extensions', 'IPv6 privacy extensions', {
- enum: ['yes', 'no', 'prefer-public']
- });
- this.IPv6AcceptRA = new BooleanYesNo();
- this.LLMNR = new BaseField(null, 'llmnr', 'LLMNR support', {
- enum: ['yes', 'no', 'resolve']
- });
- this.MulticastDNS = new BaseField(null, 'mdns', 'Multicast DNS', {
- enum: ['yes', 'no', 'resolve']
- });
- this.DNSSEC = new BaseField(null, 'dnssec', 'DNSSEC support', {
- enum: ['yes', 'no', 'allow-downgrade']
- });
- this.Domains = new BaseField(null, 'strings', 'DNS search domains');
- this.ConfigureWithoutCarrier = new BooleanYesNo();
- this.IgnoreCarrierLoss = new BooleanYesNo();
- this.KeepConfiguration = new BaseField(null, 'number', 'Keep configuration time in seconds');
- }
+ constructor() {
+ this.Description = new BaseField(
+ null,
+ FieldType.STRING,
+ "Interface description",
+ );
+ this.DHCP = new BaseField(null, FieldType.DHCP_MODE, "DHCP client", {
+ enum: Object.values(DHCPMode),
+ });
+ this.DHCPServer = new BooleanYesNo();
+ this.DNS = new BaseField(null, FieldType.IP_ADDRESSES, "DNS servers");
+ this.NTP = new BaseField(null, FieldType.IP_ADDRESSES, "NTP servers");
+ this.IPForward = new BaseField(
+ null,
+ FieldType.IP_FORWARD,
+ "IP forwarding",
+ {
+ enum: Object.values(IPForward),
+ },
+ );
+ this.IPv6PrivacyExtensions = new BaseField(
+ null,
+ FieldType.PRIVACY_EXTENSIONS,
+ "IPv6 privacy extensions",
+ {
+ enum: Object.values(IPv6PrivacyExtensions),
+ },
+ );
+ this.IPv6AcceptRA = new BooleanYesNo();
+ this.LLMNR = new BaseField(null, FieldType.LLMNR, "LLMNR support", {
+ enum: Object.values(LLMNROptions),
+ });
+ this.MulticastDNS = new BaseField(null, FieldType.MDNS, "Multicast DNS", {
+ enum: Object.values(MulticastDNS),
+ });
+ this.DNSSEC = new BaseField(null, FieldType.DNSSEC, "DNSSEC support", {
+ enum: Object.values(DNSSECOptions),
+ });
+ this.Domains = new BaseField(null, FieldType.STRINGS, "DNS search domains");
+ this.ConfigureWithoutCarrier = new BooleanYesNo();
+ this.IgnoreCarrierLoss = new BooleanYesNo();
+ this.KeepConfiguration = new BaseField(
+ null,
+ FieldType.NUMBER,
+ "Keep configuration time in seconds",
+ );
+ this.LLDP = new BooleanYesNo();
+ this.EmitLLDP = new BooleanYesNo();
+ }
}
/**
@@ -209,19 +328,142 @@ class NetworkSection {
* @class DHCPSection
*/
class DHCPSection {
- constructor() {
- this.UseDNS = new BooleanYesNo();
- this.UseNTP = new BooleanYesNo();
- this.UseMTU = new BooleanYesNo();
- this.UseHostname = new BooleanYesNo();
- this.UseDomains = new BaseField(null, 'use-domains', 'Use domains from DHCP', {
- enum: ['yes', 'no', 'route']
- });
- this.ClientIdentifier = new BaseField(null, 'client-identifier', 'DHCP client identifier', {
- enum: ['mac', 'duid']
- });
- this.RouteMetric = new BaseField(null, 'number', 'Route metric for DHCP routes');
- }
+ constructor() {
+ this.UseDNS = new BooleanYesNo();
+ this.UseNTP = new BooleanYesNo();
+ this.UseMTU = new BooleanYesNo();
+ this.UseHostname = new BooleanYesNo();
+ this.UseDomains = new BaseField(
+ null,
+ FieldType.USE_DOMAINS,
+ "Use domains from DHCP",
+ {
+ enum: Object.values(UseDomains),
+ },
+ );
+ this.ClientIdentifier = new BaseField(
+ null,
+ FieldType.CLIENT_IDENTIFIER,
+ "DHCP client identifier",
+ {
+ enum: Object.values(ClientIdentifier),
+ },
+ );
+ this.RouteMetric = new BaseField(
+ null,
+ FieldType.NUMBER,
+ "Route metric for DHCP routes",
+ );
+ this.UseRoutes = new BooleanYesNo();
+ this.SendRelease = new BooleanYesNo();
+ }
+}
+
+/**
+ * [DHCPv4] section configuration
+ * @class DHCPv4Section
+ */
+class DHCPv4Section {
+ constructor() {
+ this.ClientIdentifier = new BaseField(
+ null,
+ FieldType.CLIENT_IDENTIFIER,
+ "DHCPv4 client identifier",
+ {
+ enum: Object.values(ClientIdentifier),
+ },
+ );
+ this.UseDNS = new BooleanYesNo();
+ this.UseNTP = new BooleanYesNo();
+ this.UseMTU = new BooleanYesNo();
+ this.UseHostname = new BooleanYesNo();
+ this.UseDomains = new BaseField(
+ null,
+ FieldType.USE_DOMAINS,
+ "Use domains from DHCPv4",
+ {
+ enum: Object.values(UseDomains),
+ },
+ );
+ this.SendRelease = new BooleanYesNo();
+ }
+}
+
+/**
+ * [DHCPv6] section configuration
+ * @class DHCPv6Section
+ */
+class DHCPv6Section {
+ constructor() {
+ this.UseDNS = new BooleanYesNo();
+ this.UseNTP = new BooleanYesNo();
+ this.UseHostname = new BooleanYesNo();
+ this.UseDomains = new BaseField(
+ null,
+ FieldType.USE_DOMAINS,
+ "Use domains from DHCPv6",
+ {
+ enum: Object.values(UseDomains),
+ },
+ );
+ this.WithoutRA = new BooleanYesNo();
+ this.UseAddress = new BooleanYesNo();
+ }
+}
+
+/**
+ * [IPv6AcceptRA] section configuration
+ * @class IPv6AcceptRASection
+ */
+class IPv6AcceptRASection {
+ constructor() {
+ this.UseDNS = new BooleanYesNo();
+ this.UseDomains = new BaseField(
+ null,
+ FieldType.USE_DOMAINS,
+ "Use domains from RA",
+ {
+ enum: Object.values(UseDomains),
+ },
+ );
+ this.UseAutonomousPrefix = new BooleanYesNo();
+ this.UseOnLinkPrefix = new BooleanYesNo();
+ this.UseRoutePrefix = new BooleanYesNo();
+ this.RouteMetric = new BaseField(
+ null,
+ FieldType.NUMBER,
+ "Route metric for RA routes",
+ );
+ }
+}
+
+/**
+ * [SLAAC] section configuration
+ * @class SLAACSection
+ */
+class SLAACSection {
+ constructor() {
+ this.UseDNS = new BooleanYesNo();
+ this.UseDomains = new BaseField(
+ null,
+ FieldType.USE_DOMAINS,
+ "Use domains from SLAAC",
+ {
+ enum: Object.values(UseDomains),
+ },
+ );
+ this.UseAddress = new BooleanYesNo();
+ this.RouteMetric = new BaseField(
+ null,
+ FieldType.NUMBER,
+ "Route metric for SLAAC routes",
+ );
+ this.Critical = new BooleanYesNo();
+ this.PreferTemporaryAddress = new BooleanYesNo();
+ this.UseAutonomousPrefix = new BooleanYesNo();
+ this.UseOnLinkPrefix = new BooleanYesNo();
+ this.UseRoutePrefix = new BooleanYesNo();
+ }
}
/**
@@ -229,14 +471,23 @@ class DHCPSection {
* @class AddressSection
*/
class AddressSection {
- constructor() {
- this.Address = new BaseField(null, 'ip-prefix', 'IP address with prefix');
- this.Peer = new BaseField(null, 'ip-address', 'Peer address');
- this.Broadcast = new BaseField(null, 'ip-address', 'Broadcast address');
- this.Label = new BaseField(null, 'string', 'Address label');
- this.Scope = new BaseField(null, 'number', 'Address scope');
- this.Flags = new BaseField(null, 'strings', 'Address flags');
- }
+ constructor() {
+ this.Address = new BaseField(
+ null,
+ FieldType.IP_PREFIX,
+ "IP address with prefix",
+ );
+ this.Peer = new BaseField(null, FieldType.IP_ADDRESS, "Peer address");
+ this.Broadcast = new BaseField(
+ null,
+ FieldType.IP_ADDRESS,
+ "Broadcast address",
+ );
+ this.Label = new BaseField(null, FieldType.STRING, "Address label");
+ this.Scope = new BaseField(null, FieldType.NUMBER, "Address scope");
+ this.Flags = new BaseField(null, FieldType.STRINGS, "Address flags");
+ this.Lifetime = new BaseField(null, FieldType.STRING, "Address lifetime");
+ }
}
/**
@@ -244,20 +495,40 @@ class AddressSection {
* @class RouteSection
*/
class RouteSection {
- constructor() {
- this.Gateway = new BaseField(null, 'ip-address', 'Gateway address');
- this.GatewayOnLink = new BooleanYesNo();
- this.Destination = new BaseField(null, 'ip-prefix', 'Destination prefix');
- this.Source = new BaseField(null, 'ip-address', 'Source address');
- this.PreferredSource = new BaseField(null, 'ip-address', 'Preferred source address');
- this.Metric = new BaseField(null, 'number', 'Route metric');
- this.Scope = new BaseField(null, 'route-scope', 'Route scope', {
- enum: ['global', 'link', 'host']
- });
- this.Type = new BaseField(null, 'route-type', 'Route type', {
- enum: ['unicast', 'local', 'broadcast', 'anycast', 'multicast', 'blackhole', 'unreachable', 'prohibit']
- });
- }
+ constructor() {
+ this.Gateway = new BaseField(null, FieldType.IP_ADDRESS, "Gateway address");
+ this.GatewayOnLink = new BooleanYesNo();
+ this.Destination = new BaseField(
+ null,
+ FieldType.IP_PREFIX,
+ "Destination prefix",
+ );
+ this.Source = new BaseField(null, FieldType.IP_ADDRESS, "Source address");
+ this.PreferredSource = new BaseField(
+ null,
+ FieldType.IP_ADDRESS,
+ "Preferred source address",
+ );
+ this.Metric = new BaseField(null, FieldType.NUMBER, "Route metric");
+ this.Scope = new BaseField(null, FieldType.ROUTE_SCOPE, "Route scope", {
+ enum: Object.values(RouteScope),
+ });
+ this.Type = new BaseField(null, FieldType.ROUTE_TYPE, "Route type", {
+ enum: Object.values(RouteType),
+ });
+ this.InitialCongestionWindow = new BaseField(
+ null,
+ FieldType.NUMBER,
+ "Initial congestion window",
+ );
+ this.InitialAdvertisedReceiveWindow = new BaseField(
+ null,
+ FieldType.NUMBER,
+ "Initial advertised receive window",
+ );
+ this.Table = new BaseField(null, FieldType.NUMBER, "Routing table");
+ this.Protocol = new BaseField(null, FieldType.NUMBER, "Routing protocol");
+ }
}
/**
@@ -265,264 +536,432 @@ class RouteSection {
* @class NetworkConfiguration
*/
class NetworkConfiguration {
- constructor() {
- this.Match = new MatchSection();
- this.Link = new LinkSection();
- this.Network = new NetworkSection();
- this.DHCP = new DHCPSection();
- this.Address = [];
- this.Route = [];
- }
-
- /**
- * Get schema for structured editor
- * @returns {Object} Schema definition
- */
- getSchema() {
- return {
- Match: this._getSectionSchema(this.Match),
- Link: this._getSectionSchema(this.Link),
- Network: this._getSectionSchema(this.Network),
- DHCP: this._getSectionSchema(this.DHCP),
- Address: this._getArraySectionSchema(AddressSection, 'Address'),
- Route: this._getArraySectionSchema(RouteSection, 'Route')
- };
- }
-
- /**
- * Get schema for a single section
- * @private
- * @param {Object} section - Section instance
- * @returns {Object} Section schema
- */
- _getSectionSchema(section) {
- const schema = {};
- for (const [key, field] of Object.entries(section)) {
- schema[key] = {
- value: field.value,
- type: field.type,
- description: field.description,
- options: field.options
- };
- }
- return schema;
- }
-
- /**
- * Get schema for array sections (Address, Route)
- * @private
- * @param {Class} SectionClass - Section class
- * @param {string} sectionName - Section name
- * @returns {Object} Array section schema
- */
- _getArraySectionSchema(SectionClass, sectionName) {
- const template = new SectionClass();
- return {
- itemSchema: this._getSectionSchema(template),
- items: this[sectionName].map(item => this._getSectionSchema(item))
- };
- }
-
- /**
- * Parse systemd network configuration from text
- * @param {string} configText - Configuration file content
- * @returns {NetworkConfiguration}
- */
- static fromSystemdConfiguration(configText) {
- const config = new NetworkConfiguration();
- const lines = configText.split('\n');
- let currentSection = null;
- let currentAddress = null;
- let currentRoute = null;
-
- for (const line of lines) {
- const trimmed = line.trim();
-
- // Skip empty lines and comments
- if (!trimmed || trimmed.startsWith('#')) continue;
-
- // Section header
- const sectionMatch = trimmed.match(/^\[(\w+)\]$/);
- if (sectionMatch) {
- currentSection = sectionMatch[1].toLowerCase();
-
- // Start new array sections
- if (currentSection === 'address') {
- currentAddress = new AddressSection();
- config.Address.push(currentAddress);
- } else if (currentSection === 'route') {
- currentRoute = new RouteSection();
- config.Route.push(currentRoute);
- }
- continue;
- }
-
- // Key-value pair
- const kvMatch = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
- if (kvMatch && currentSection) {
- const key = kvMatch[1];
- const value = kvMatch[2];
-
- config._setValue(currentSection, key, value, currentAddress, currentRoute);
- }
- }
-
- return config;
- }
-
- /**
- * Set configuration value
- * @private
- * @param {string} section - Section name
- * @param {string} key - Key name
- * @param {string} value - Value
- * @param {AddressSection} currentAddress - Current address section
- * @param {RouteSection} currentRoute - Current route section
- */
- _setValue(section, key, value, currentAddress, currentRoute) {
- switch (section) {
- case 'match':
- this._setMatchValue(key, value);
- break;
- case 'link':
- this._setLinkValue(key, value);
- break;
- case 'network':
- this._setNetworkValue(key, value);
- break;
- case 'dhcp':
- this._setDHCPValue(key, value);
- break;
- case 'address':
- if (currentAddress) {
- this._setAddressValue(currentAddress, key, value);
- }
- break;
- case 'route':
- if (currentRoute) {
- this._setRouteValue(currentRoute, key, value);
- }
- break;
- }
- }
-
- _setMatchValue(key, value) {
- if (this.Match[key] !== undefined) {
- this.Match[key].value = value;
- }
- }
-
- _setLinkValue(key, value) {
- if (this.Link[key] !== undefined) {
- this.Link[key].value = value;
- }
- }
-
- _setNetworkValue(key, value) {
- if (this.Network[key] !== undefined) {
- this.Network[key].value = value;
- }
- }
-
- _setDHCPValue(key, value) {
- if (this.DHCP[key] !== undefined) {
- this.DHCP[key].value = value;
- }
- }
-
- _setAddressValue(address, key, value) {
- if (address[key] !== undefined) {
- address[key].value = value;
- }
- }
-
- _setRouteValue(route, key, value) {
- if (route[key] !== undefined) {
- route[key].value = value;
- }
- }
-
- /**
- * Convert to systemd network configuration format
- * @returns {string}
- */
- toSystemdConfiguration() {
- const sections = [];
-
- // [Match] section
- if (this._hasSectionValues(this.Match)) {
- sections.push('[Match]');
- sections.push(...this._formatSection(this.Match));
- }
-
- // [Link] section
- if (this._hasSectionValues(this.Link)) {
- sections.push('[Link]');
- sections.push(...this._formatSection(this.Link));
- }
-
- // [Network] section
- if (this._hasSectionValues(this.Network)) {
- sections.push('[Network]');
- sections.push(...this._formatSection(this.Network));
- }
-
- // [DHCP] section
- if (this._hasSectionValues(this.DHCP)) {
- sections.push('[DHCP]');
- sections.push(...this._formatSection(this.DHCP));
- }
-
- // [Address] sections
- this.Address.forEach(addr => {
- if (this._hasSectionValues(addr)) {
- sections.push('[Address]');
- sections.push(...this._formatSection(addr));
- }
- });
-
- // [Route] sections
- this.Route.forEach(route => {
- if (this._hasSectionValues(route)) {
- sections.push('[Route]');
- sections.push(...this._formatSection(route));
- }
- });
-
- return `${sections.join('\n')}\n`;
- }
-
- _hasSectionValues(section) {
- return Object.values(section).some(field =>
- field.value !== null && field.value !== undefined && field.value !== ''
- );
- }
-
- _formatSection(section) {
- const lines = [];
- for (const [key, field] of Object.entries(section)) {
- if (field.value !== null && field.value !== undefined && field.value !== '') {
- lines.push(`${key}=${field.toString()}`);
- }
- }
- return lines;
- }
+ #match;
+ #link;
+ #network;
+ #dhcp;
+ #dhcpv4;
+ #dhcpv6;
+ #ipv6AcceptRA;
+ #slaac;
+ #address;
+ #route;
+
+ constructor() {
+ this.#match = new MatchSection();
+ this.#link = new LinkSection();
+ this.#network = new NetworkSection();
+ this.#dhcp = new DHCPSection();
+ this.#dhcpv4 = new DHCPv4Section();
+ this.#dhcpv6 = new DHCPv6Section();
+ this.#ipv6AcceptRA = new IPv6AcceptRASection();
+ this.#slaac = new SLAACSection();
+ this.#address = [];
+ this.#route = [];
+ }
+
+ /**
+ * Get Match section
+ * @returns {MatchSection}
+ */
+ get Match() {
+ return this.#match;
+ }
+
+ /**
+ * Get Link section
+ * @returns {LinkSection}
+ */
+ get Link() {
+ return this.#link;
+ }
+
+ /**
+ * Get Network section
+ * @returns {NetworkSection}
+ */
+ get Network() {
+ return this.#network;
+ }
+
+ /**
+ * Get DHCP section
+ * @returns {DHCPSection}
+ */
+ get DHCP() {
+ return this.#dhcp;
+ }
+
+ /**
+ * Get DHCPv4 section
+ * @returns {DHCPv4Section}
+ */
+ get DHCPv4() {
+ return this.#dhcpv4;
+ }
+
+ /**
+ * Get DHCPv6 section
+ * @returns {DHCPv6Section}
+ */
+ get DHCPv6() {
+ return this.#dhcpv6;
+ }
+
+ /**
+ * Get IPv6AcceptRA section
+ * @returns {IPv6AcceptRASection}
+ */
+ get IPv6AcceptRA() {
+ return this.#ipv6AcceptRA;
+ }
+
+ /**
+ * Get SLAAC section
+ * @returns {SLAACSection}
+ */
+ get SLAAC() {
+ return this.#slaac;
+ }
+
+ /**
+ * Get Address sections
+ * @returns {Array<AddressSection>}
+ */
+ get Address() {
+ return [...this.#address];
+ }
+
+ /**
+ * Get Route sections
+ * @returns {Array<RouteSection>}
+ */
+ get Route() {
+ return [...this.#route];
+ }
+
+ /**
+ * Get schema for structured editor
+ * @returns {Object} Schema definition
+ */
+ getSchema() {
+ return {
+ Match: this.#getSectionSchema(this.#match),
+ Link: this.#getSectionSchema(this.#link),
+ Network: this.#getSectionSchema(this.#network),
+ DHCP: this.#getSectionSchema(this.#dhcp),
+ DHCPv4: this.#getSectionSchema(this.#dhcpv4),
+ DHCPv6: this.#getSectionSchema(this.#dhcpv6),
+ IPv6AcceptRA: this.#getSectionSchema(this.#ipv6AcceptRA),
+ SLAAC: this.#getSectionSchema(this.#slaac),
+ Address: this.#getArraySectionSchema(AddressSection, "Address"),
+ Route: this.#getArraySectionSchema(RouteSection, "Route"),
+ };
+ }
+
+ /**
+ * Get schema for a single section
+ * @private
+ * @param {Object} section - Section instance
+ * @returns {Object} Section schema
+ */
+ #getSectionSchema(section) {
+ const schema = {};
+ for (const [key, field] of Object.entries(section)) {
+ schema[key] = {
+ value: field.value,
+ type: field.type,
+ description: field.description,
+ options: field.options,
+ };
+ }
+ return schema;
+ }
+
+ /**
+ * Get schema for array sections (Address, Route)
+ * @private
+ * @param {Class} SectionClass - Section class
+ * @param {string} sectionName - Section name
+ * @returns {Object} Array section schema
+ */
+ #getArraySectionSchema(SectionClass, sectionName) {
+ const template = new SectionClass();
+ return {
+ itemSchema: this.#getSectionSchema(template),
+ items: this[sectionName].map((item) => this.#getSectionSchema(item)),
+ };
+ }
+
+ /**
+ * Parse systemd network configuration from text
+ * @static
+ * @param {string} configText - Configuration file content
+ * @returns {NetworkConfiguration}
+ */
+ static fromSystemdConfiguration(configText) {
+ const config = new NetworkConfiguration();
+ const lines = configText.split("\n");
+ let currentSection = null;
+ let currentAddress = null;
+ let currentRoute = null;
+
+ for (const line of lines) {
+ const trimmed = line.trim();
+
+ // Skip empty lines and comments
+ if (!trimmed || trimmed.startsWith("#")) continue;
+
+ // Section header
+ const sectionMatch = trimmed.match(/^\[(\w+)\]$/);
+ if (sectionMatch) {
+ currentSection = sectionMatch[1].toLowerCase();
+
+ // Start new array sections
+ if (currentSection === "address") {
+ currentAddress = new AddressSection();
+ config.#address.push(currentAddress);
+ } else if (currentSection === "route") {
+ currentRoute = new RouteSection();
+ config.#route.push(currentRoute);
+ }
+ continue;
+ }
+
+ // Key-value pair
+ const kvMatch = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
+ if (kvMatch && currentSection) {
+ const key = kvMatch[1];
+ const value = kvMatch[2];
+
+ config.#setValue(
+ currentSection,
+ key,
+ value,
+ currentAddress,
+ currentRoute,
+ );
+ }
+ }
+
+ return config;
+ }
+
+ /**
+ * Set configuration value
+ * @private
+ * @param {string} section - Section name
+ * @param {string} key - Key name
+ * @param {string} value - Value
+ * @param {AddressSection} currentAddress - Current address section
+ * @param {RouteSection} currentRoute - Current route section
+ */
+ #setValue(section, key, value, currentAddress, currentRoute) {
+ switch (section) {
+ case "match":
+ this.#setMatchValue(key, value);
+ break;
+ case "link":
+ this.#setLinkValue(key, value);
+ break;
+ case "network":
+ this.#setNetworkValue(key, value);
+ break;
+ case "dhcp":
+ this.#setDHCPValue(key, value);
+ break;
+ case "dhcpv4":
+ this.#setDHCPv4Value(key, value);
+ break;
+ case "dhcpv6":
+ this.#setDHCPv6Value(key, value);
+ break;
+ case "ipv6acceptra":
+ this.#setIPv6AcceptRAValue(key, value);
+ break;
+ case "slaac":
+ this.#setSLAACValue(key, value);
+ break;
+ case "address":
+ if (currentAddress) {
+ this.#setAddressValue(currentAddress, key, value);
+ }
+ break;
+ case "route":
+ if (currentRoute) {
+ this.#setRouteValue(currentRoute, key, value);
+ }
+ break;
+ }
+ }
+
+ #setMatchValue(key, value) {
+ if (this.#match[key] !== undefined) {
+ this.#match[key].value = value;
+ }
+ }
+
+ #setLinkValue(key, value) {
+ if (this.#link[key] !== undefined) {
+ this.#link[key].value = value;
+ }
+ }
+
+ #setNetworkValue(key, value) {
+ if (this.#network[key] !== undefined) {
+ this.#network[key].value = value;
+ }
+ }
+
+ #setDHCPValue(key, value) {
+ if (this.#dhcp[key] !== undefined) {
+ this.#dhcp[key].value = value;
+ }
+ }
+
+ #setDHCPv4Value(key, value) {
+ if (this.#dhcpv4[key] !== undefined) {
+ this.#dhcpv4[key].value = value;
+ }
+ }
+
+ #setDHCPv6Value(key, value) {
+ if (this.#dhcpv6[key] !== undefined) {
+ this.#dhcpv6[key].value = value;
+ }
+ }
+
+ #setIPv6AcceptRAValue(key, value) {
+ if (this.#ipv6AcceptRA[key] !== undefined) {
+ this.#ipv6AcceptRA[key].value = value;
+ }
+ }
+
+ #setSLAACValue(key, value) {
+ if (this.#slaac[key] !== undefined) {
+ this.#slaac[key].value = value;
+ }
+ }
+
+ #setAddressValue(address, key, value) {
+ if (address[key] !== undefined) {
+ address[key].value = value;
+ }
+ }
+
+ #setRouteValue(route, key, value) {
+ if (route[key] !== undefined) {
+ route[key].value = value;
+ }
+ }
+
+ /**
+ * Convert to systemd network configuration format
+ * @returns {string}
+ */
+ toSystemdConfiguration() {
+ const sections = [];
+
+ // [Match] section
+ if (this.#hasSectionValues(this.#match)) {
+ sections.push("[Match]");
+ sections.push(...this.#formatSection(this.#match));
+ }
+
+ // [Link] section
+ if (this.#hasSectionValues(this.#link)) {
+ sections.push("[Link]");
+ sections.push(...this.#formatSection(this.#link));
+ }
+
+ // [Network] section
+ if (this.#hasSectionValues(this.#network)) {
+ sections.push("[Network]");
+ sections.push(...this.#formatSection(this.#network));
+ }
+
+ // [DHCP] section
+ if (this.#hasSectionValues(this.#dhcp)) {
+ sections.push("[DHCP]");
+ sections.push(...this.#formatSection(this.#dhcp));
+ }
+
+ // [DHCPv4] section
+ if (this.#hasSectionValues(this.#dhcpv4)) {
+ sections.push("[DHCPv4]");
+ sections.push(...this.#formatSection(this.#dhcpv4));
+ }
+
+ // [DHCPv6] section
+ if (this.#hasSectionValues(this.#dhcpv6)) {
+ sections.push("[DHCPv6]");
+ sections.push(...this.#formatSection(this.#dhcpv6));
+ }
+
+ // [IPv6AcceptRA] section
+ if (this.#hasSectionValues(this.#ipv6AcceptRA)) {
+ sections.push("[IPv6AcceptRA]");
+ sections.push(...this.#formatSection(this.#ipv6AcceptRA));
+ }
+
+ // [SLAAC] section
+ if (this.#hasSectionValues(this.#slaac)) {
+ sections.push("[SLAAC]");
+ sections.push(...this.#formatSection(this.#slaac));
+ }
+
+ // [Address] sections
+ this.#address.forEach((addr) => {
+ if (this.#hasSectionValues(addr)) {
+ sections.push("[Address]");
+ sections.push(...this.#formatSection(addr));
+ }
+ });
+
+ // [Route] sections
+ this.#route.forEach((route) => {
+ if (this.#hasSectionValues(route)) {
+ sections.push("[Route]");
+ sections.push(...this.#formatSection(route));
+ }
+ });
+
+ return sections.length > 0 ? `${sections.join("\n")}\n` : "";
+ }
+
+ #hasSectionValues(section) {
+ return Object.values(section).some((field) => field.hasValue());
+ }
+
+ #formatSection(section) {
+ const lines = [];
+ for (const [key, field] of Object.entries(section)) {
+ if (field.hasValue()) {
+ lines.push(`${key}=${field.toString()}`);
+ }
+ }
+ return lines;
+ }
}
// Export classes
export {
- BaseField,
- MACAddress,
- IPv4Address,
- IPv6Address,
- BooleanYesNo,
- Port,
- MTU,
- MatchSection,
- LinkSection,
- NetworkSection,
- DHCPSection,
- AddressSection,
- RouteSection,
- NetworkConfiguration
+ BaseField,
+ MACAddress,
+ IPv4Address,
+ IPv6Address,
+ BooleanYesNo,
+ Port,
+ MTU,
+ MatchSection,
+ LinkSection,
+ NetworkSection,
+ DHCPSection,
+ DHCPv4Section,
+ DHCPv6Section,
+ IPv6AcceptRASection,
+ SLAACSection,
+ AddressSection,
+ RouteSection,
+ NetworkConfiguration,
};
diff --git a/static/theme-manager.js b/static/theme-manager.js
index 2ad080b..14e1469 100644
--- a/static/theme-manager.js
+++ b/static/theme-manager.js
@@ -1,59 +1,177 @@
/* jshint esversion: 2024, module: true */
+import { ThemeMode } from "./enums.js";
+
+/**
+ * Static Theme Utilities
+ * @class ThemeUtils
+ */
+class ThemeUtils {
+ /**
+ * Get theme from localStorage
+ * @static
+ * @returns {Symbol} Theme mode
+ */
+ static getStoredTheme() {
+ const stored = localStorage.getItem("network-ui-theme");
+ return stored === "light" ? ThemeMode.LIGHT : ThemeMode.DARK;
+ }
+
+ /**
+ * Store theme in localStorage
+ * @static
+ * @param {Symbol} theme - Theme mode
+ */
+ static storeTheme(theme) {
+ const themeString = theme === ThemeMode.LIGHT ? "light" : "dark";
+ localStorage.setItem("network-ui-theme", themeString);
+ }
+
+ /**
+ * Get theme icon based on theme mode
+ * @static
+ * @param {Symbol} theme - Theme mode
+ * @returns {string} Icon character
+ */
+ static getThemeIcon(theme) {
+ return theme === ThemeMode.DARK ? "☀️" : "🌙";
+ }
+
+ /**
+ * Apply theme to document
+ * @static
+ * @param {Symbol} theme - Theme mode
+ */
+ static applyThemeToDocument(theme) {
+ const themeString = theme === ThemeMode.LIGHT ? "light" : "dark";
+ document.documentElement.setAttribute("data-theme", themeString);
+ }
+
+ /**
+ * Toggle theme mode
+ * @static
+ * @param {Symbol} currentTheme - Current theme mode
+ * @returns {Symbol} New theme mode
+ */
+ static toggleTheme(currentTheme) {
+ return currentTheme === ThemeMode.DARK ? ThemeMode.LIGHT : ThemeMode.DARK;
+ }
+}
+
/**
* Theme Manager for handling light/dark themes
* @class ThemeManager
*/
class ThemeManager {
- /**
- * @param {Object} elements - DOM elements
- */
- constructor(elements) {
- this.elements = elements;
- this.theme = localStorage.getItem('network-ui-theme') || 'dark';
- }
-
- /**
- * Initialize theme manager
- * @method init
- */
- init() {
- this.applyTheme(this.theme);
- this.setupEventListeners();
- }
-
- /**
- * Set up theme event listeners
- * @method setupEventListeners
- */
- setupEventListeners() {
- this.elements.themeToggle?.addEventListener('click', () => this.toggleTheme());
- }
-
- /**
- * Toggle between light and dark themes
- * @method toggleTheme
- */
- toggleTheme() {
- const newTheme = this.theme === 'dark' ? 'light' : 'dark';
- this.applyTheme(newTheme);
- }
-
- /**
- * Apply theme to document
- * @method applyTheme
- * @param {string} theme - Theme name ('light' or 'dark')
- */
- applyTheme(theme) {
- document.documentElement.setAttribute('data-theme', theme);
- this.theme = theme;
- localStorage.setItem('network-ui-theme', theme);
-
- // Update theme icon
- if (this.elements.themeIcon) {
- this.elements.themeIcon.textContent = theme === 'dark' ? '☀️' : '🌙';
- }
- }
+ #elements;
+ #theme;
+
+ /**
+ * @param {Object} elements - DOM elements
+ */
+ constructor(elements) {
+ this.#elements = elements;
+ this.#theme = ThemeMode.DARK;
+ }
+
+ /**
+ * Initialize theme manager
+ * @method init
+ */
+ init() {
+ this.#theme = ThemeUtils.getStoredTheme();
+ this.#applyTheme(this.#theme);
+ this.#setupEventListeners();
+ }
+
+ /**
+ * Set up theme event listeners
+ * @private
+ */
+ #setupEventListeners() {
+ this.#elements.themeToggle?.addEventListener("click", () =>
+ this.#handleThemeToggle(),
+ );
+ }
+
+ /**
+ * Handle theme toggle
+ * @private
+ */
+ #handleThemeToggle() {
+ const newTheme = ThemeUtils.toggleTheme(this.#theme);
+ this.#applyTheme(newTheme);
+ }
+
+ /**
+ * Apply theme to UI
+ * @private
+ * @param {Symbol} theme - Theme mode
+ */
+ #applyTheme(theme) {
+ this.#theme = theme;
+
+ // Apply to document
+ ThemeUtils.applyThemeToDocument(theme);
+
+ // Store preference
+ ThemeUtils.storeTheme(theme);
+
+ // Update theme icon
+ this.#updateThemeIcon();
+ }
+
+ /**
+ * Update theme icon
+ * @private
+ */
+ #updateThemeIcon() {
+ if (this.#elements.themeIcon) {
+ this.#elements.themeIcon.textContent = ThemeUtils.getThemeIcon(
+ this.#theme,
+ );
+ }
+ }
+
+ /**
+ * Get current theme
+ * @method getCurrentTheme
+ * @returns {Symbol} Current theme mode
+ */
+ getCurrentTheme() {
+ return this.#theme;
+ }
+
+ /**
+ * Set theme programmatically
+ * @method setTheme
+ * @param {Symbol} theme - Theme mode
+ */
+ setTheme(theme) {
+ if (theme === ThemeMode.LIGHT || theme === ThemeMode.DARK) {
+ this.#applyTheme(theme);
+ } else {
+ console.warn("Invalid theme mode:", theme);
+ }
+ }
+
+ /**
+ * Check if dark theme is active
+ * @method isDarkTheme
+ * @returns {boolean} True if dark theme is active
+ */
+ isDarkTheme() {
+ return this.#theme === ThemeMode.DARK;
+ }
+
+ /**
+ * Check if light theme is active
+ * @method isLightTheme
+ * @returns {boolean} True if light theme is active
+ */
+ isLightTheme() {
+ return this.#theme === ThemeMode.LIGHT;
+ }
}
-export { ThemeManager };
+export { ThemeManager, ThemeUtils };
diff --git a/static/utils.js b/static/utils.js
index 2b0b816..32b10be 100644
--- a/static/utils.js
+++ b/static/utils.js
@@ -1,155 +1,173 @@
/* jshint esversion: 2024, module: true */
-import { InterfaceState } from './enums.js';
+import { InterfaceState } from "./enums.js";
/**
* Static utility functions
* @module Utils
*/
class Utils {
- /**
- * Convert byte array to MAC address
- * @static
- * @param {Array} bytes - Byte array
- * @returns {string} MAC address
- */
- static arrayToMac(bytes) {
- if (!Array.isArray(bytes)) return '';
- return bytes.map(byte => byte.toString(16).padStart(2, '0')).join(':');
- }
-
- /**
- * Convert byte array to IP address
- * @static
- * @param {Array|Object} obj - IP data
- * @returns {string} IP address
- */
- static ipFromArray(obj) {
- let bytes = null;
-
- if (Array.isArray(obj)) {
- bytes = obj;
- } else if (obj?.Address && Array.isArray(obj.Address)) {
- bytes = obj.Address;
- } else {
- return '';
- }
-
- // IPv4
- if (bytes.length === 4) {
- return bytes.join('.');
- }
-
- // IPv6
- if (bytes.length === 16) {
- const parts = [];
- for (let i = 0; i < 16; i += 2) {
- parts.push(((bytes[i] << 8) | bytes[i + 1]).toString(16));
- }
- return parts.join(':').replace(/(^|:)0+/g, '$1').replace(/:{3,}/, '::');
- }
-
- return '';
- }
-
- /**
- * Convert route object to string
- * @static
- * @param {Object} route - Route object
- * @returns {string} Route string
- */
- static routeToString(route) {
- if (!route) return '';
- const destination = route.Destination ? Utils.ipFromArray(route.Destination) : 'default';
- const gateway = route.Gateway ? Utils.ipFromArray(route.Gateway) : '';
- return gateway ? `${destination} → ${gateway}` : destination;
- }
-
- /**
- * Get interface state from interface object
- * @static
- * @param {Object} iface - Interface object
- * @returns {Symbol} Interface state
- */
- static getInterfaceState(iface) {
- const state = iface.OperationalState ?? iface.AdministrativeState ?? iface.State ?? '';
- const stateLower = state.toLowerCase();
-
- if (stateLower.includes('up') || stateLower.includes('routable') || stateLower.includes('configured')) {
- return InterfaceState.UP;
- } else if (stateLower.includes('down') || stateLower.includes('off')) {
- return InterfaceState.DOWN;
- } else {
- return InterfaceState.UNKNOWN;
- }
- }
-
- /**
- * Get CSS class for interface state
- * @static
- * @param {Symbol} state - Interface state
- * @returns {string} CSS class
- */
- static getStateClass(state) {
- switch (state) {
- case InterfaceState.UP: return 'state-up';
- case InterfaceState.DOWN: return 'state-down';
- default: return 'state-unknown';
- }
- }
-
- /**
- * Get display text for interface state
- * @static
- * @param {Object} iface - Interface object
- * @returns {string} State text
- */
- static getStateText(iface) {
- return iface.OperationalState ?? iface.AdministrativeState ?? iface.State ?? 'unknown';
- }
-
- /**
- * Sanitize HTML string
- * @static
- * @param {string} str - String to sanitize
- * @returns {string} Sanitized string
- */
- static sanitizeHTML(str) {
- const div = document.createElement('div');
- div.textContent = str;
- return div.innerHTML;
- }
-
- /**
- * Create DOM element from HTML string
- * @static
- * @param {string} html - HTML string
- * @returns {HTMLElement} DOM element
- */
- static createElementFromHTML(html) {
- const template = document.createElement('template');
- template.innerHTML = html.trim();
- return template.content.firstElementChild;
- }
-
- /**
- * Debounce function
- * @static
- * @param {Function} func - Function to debounce
- * @param {number} wait - Wait time in ms
- * @returns {Function} Debounced function
- */
- static debounce(func, wait) {
- let timeout;
- return function executedFunction(...args) {
- const later = () => {
- clearTimeout(timeout);
- func(...args);
- };
- clearTimeout(timeout);
- timeout = setTimeout(later, wait);
- };
- }
+ /**
+ * Convert byte array to MAC address
+ * @static
+ * @param {Array} bytes - Byte array
+ * @returns {string} MAC address
+ */
+ static arrayToMac(bytes) {
+ if (!Array.isArray(bytes)) return "";
+ return bytes.map((byte) => byte.toString(16).padStart(2, "0")).join(":");
+ }
+
+ /**
+ * Convert byte array to IP address
+ * @static
+ * @param {Array|Object} obj - IP data
+ * @returns {string} IP address
+ */
+ static ipFromArray(obj) {
+ let bytes = null;
+
+ if (Array.isArray(obj)) {
+ bytes = obj;
+ } else if (obj?.Address && Array.isArray(obj.Address)) {
+ bytes = obj.Address;
+ } else {
+ return "";
+ }
+
+ // IPv4
+ if (bytes.length === 4) {
+ return bytes.join(".");
+ }
+
+ // IPv6
+ if (bytes.length === 16) {
+ const parts = [];
+ for (let i = 0; i < 16; i += 2) {
+ parts.push(((bytes[i] << 8) | bytes[i + 1]).toString(16));
+ }
+ return parts
+ .join(":")
+ .replace(/(^|:)0+/g, "$1")
+ .replace(/:{3,}/, "::");
+ }
+
+ return "";
+ }
+
+ /**
+ * Convert route object to string
+ * @static
+ * @param {Object} route - Route object
+ * @returns {string} Route string
+ */
+ static routeToString(route) {
+ if (!route) return "";
+ const destination = route.Destination
+ ? Utils.ipFromArray(route.Destination)
+ : "default";
+ const gateway = route.Gateway ? Utils.ipFromArray(route.Gateway) : "";
+ return gateway ? `${destination} → ${gateway}` : destination;
+ }
+
+ /**
+ * Get interface state from interface object
+ * @static
+ * @param {Object} iface - Interface object
+ * @returns {Symbol} Interface state
+ */
+ static getInterfaceState(iface) {
+ const state =
+ iface.OperationalState ?? iface.AdministrativeState ?? iface.State ?? "";
+ const stateLower = state.toLowerCase();
+
+ if (
+ stateLower.includes("up") ||
+ stateLower.includes("routable") ||
+ stateLower.includes("configured")
+ ) {
+ return InterfaceState.UP;
+ } else if (stateLower.includes("down") || stateLower.includes("off")) {
+ return InterfaceState.DOWN;
+ } else {
+ return InterfaceState.UNKNOWN;
+ }
+ }
+
+ /**
+ * Get CSS class for interface state
+ * @static
+ * @param {Symbol} state - Interface state
+ * @returns {string} CSS class
+ */
+ static getStateClass(state) {
+ switch (state) {
+ case InterfaceState.UP:
+ return "state-up";
+ case InterfaceState.DOWN:
+ return "state-down";
+ default:
+ return "state-unknown";
+ }
+ }
+
+ /**
+ * Get display text for interface state
+ * @static
+ * @param {Object} iface - Interface object
+ * @returns {string} State text
+ */
+ static getStateText(iface) {
+ return (
+ iface.OperationalState ??
+ iface.AdministrativeState ??
+ iface.State ??
+ "unknown"
+ );
+ }
+
+ /**
+ * Sanitize HTML string
+ * @static
+ * @param {string} str - String to sanitize
+ * @returns {string} Sanitized string
+ */
+ static sanitizeHTML(str) {
+ const div = document.createElement("div");
+ div.textContent = str;
+ return div.innerHTML;
+ }
+
+ /**
+ * Create DOM element from HTML string
+ * @static
+ * @param {string} html - HTML string
+ * @returns {HTMLElement} DOM element
+ */
+ static createElementFromHTML(html) {
+ const template = document.createElement("template");
+ template.innerHTML = html.trim();
+ return template.content.firstElementChild;
+ }
+
+ /**
+ * Debounce function
+ * @static
+ * @param {Function} func - Function to debounce
+ * @param {number} wait - Wait time in ms
+ * @returns {Function} Debounced function
+ */
+ static debounce(func, wait) {
+ let timeout;
+ return function executedFunction(...args) {
+ const later = () => {
+ clearTimeout(timeout);
+ func(...args);
+ };
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ };
+ }
}
-export { Utils }; \ No newline at end of file
+export { Utils };