/* jshint esversion: 2024, module: true */
import { ThemeManager } from './theme-manager.js';
import { ApiClient } from './api-client.js';
import { InterfaceRenderer } from './interface-renderer.js';
import { ConfigManager } from './config-manager.js';
import { StructuredEditor } from './structured-editor.js';
/**
* Main Application Class
* @class Application
*/
class Application {
/**
* @param {Object} elements - DOM elements
*/
constructor(elements) {
this.elements = elements;
this.state = {
currentInterface: null,
interfaces: [],
editorMode: 'raw', // 'raw' or 'structured'
currentConfigFile: null
};
// Initialize modules
this.themeManager = new ThemeManager(elements);
this.apiClient = new ApiClient();
this.interfaceRenderer = new InterfaceRenderer(elements, this.state);
this.configManager = new ConfigManager(elements, this.apiClient, this.state);
// Structured editor will be initialized after DOM is ready
this.structuredEditor = null;
// Create editor mode toggle UI
this.createEditorModeToggle();
}
/**
* Initialize the application
* @method init
*/
init() {
this.themeManager.init();
this.setupEventListeners();
this.loadStatus();
// Initialize structured editor now that DOM is ready
this.initializeStructuredEditor();
}
/**
* Create editor mode toggle UI
* @method createEditorModeToggle
*/
createEditorModeToggle() {
const editorToggleHTML = `
`;
// Insert the toggle and containers into the configs panel
const configCard = this.elements.panels.configs.querySelector('.card:last-child');
configCard.insertAdjacentHTML('afterbegin', editorToggleHTML);
// Move existing form elements to raw editor container
const rawEditorContainer = document.getElementById('rawEditorContainer');
const formGroups = Array.from(configCard.querySelectorAll('.form-group, .checkbox-group'));
const configActions = configCard.querySelector('.config-actions') ||
configCard.querySelector('div:has(> #validateConfig)');
formGroups.forEach(group => {
if (!group.closest('.editor-mode-toggle')) {
rawEditorContainer.appendChild(group);
}
});
// Move config actions if they exist
if (configActions) {
rawEditorContainer.appendChild(configActions);
}
// Update elements reference
this.elements.editorContainers = {
raw: document.getElementById('rawEditorContainer'),
structured: document.getElementById('structuredEditorContainer')
};
this.elements.editorButtons = {
raw: document.getElementById('rawEditorBtn'),
structured: document.getElementById('structuredEditorBtn')
};
}
/**
* Initialize structured editor
* @method initializeStructuredEditor
*/
initializeStructuredEditor() {
if (this.elements.editorContainers.structured) {
this.structuredEditor = new StructuredEditor(this.elements.editorContainers.structured);
} else {
console.error('Structured editor container not found');
}
}
/**
* Set up all event listeners
* @method setupEventListeners
*/
setupEventListeners() {
// Navigation
this.elements.buttons.nav.forEach(button => {
button.addEventListener('click', (event) => {
this.show(event.currentTarget.dataset.panel);
});
});
// Status panel
this.elements.buttons.refreshStatus?.addEventListener('click', () => this.loadStatus());
// Configs panel - raw editor
this.elements.buttons.refreshConfigs?.addEventListener('click',
() => this.configManager.refreshConfigs());
this.elements.buttons.saveConfig?.addEventListener('click',
() => this.handleSaveConfig());
this.elements.buttons.validateConfig?.addEventListener('click',
() => this.configManager.validateConfig());
this.elements.inputs.configSelect?.addEventListener('change',
() => this.handleConfigFileChange());
// Editor mode toggle
this.elements.editorButtons?.raw?.addEventListener('click',
() => this.setEditorMode('raw'));
this.elements.editorButtons?.structured?.addEventListener('click',
() => this.setEditorMode('structured'));
// Logs panel
this.elements.buttons.refreshLogs?.addEventListener('click', () => this.loadLogs());
// Commands panel
this.elements.buttons.restartNetworkd?.addEventListener('click', () => this.restartNetworkd());
this.elements.buttons.rebootDevice?.addEventListener('click', () => this.rebootDevice());
// Touch support
document.addEventListener('touchstart', this.handleTouchStart, { passive: true });
}
/**
* Handle configuration file change
* @method handleConfigFileChange
*/
async handleConfigFileChange() {
const name = this.elements.inputs.configSelect.value;
if (!name) return;
this.state.currentConfigFile = name;
if (this.state.editorMode === 'raw') {
await this.configManager.loadConfig();
} else {
await this.loadConfigForStructuredEditor();
}
}
/**
* Load config for structured editor
* @method loadConfigForStructuredEditor
*/
async loadConfigForStructuredEditor() {
if (!this.structuredEditor) {
console.error('Structured editor not initialized');
return;
}
try {
const text = await this.apiClient.getText(`/api/config/${encodeURIComponent(this.state.currentConfigFile)}`);
await this.structuredEditor.loadConfiguration(text, this.state.currentConfigFile);
} catch (error) {
alert(`Failed to load config for structured editor: ${error.message}`);
}
}
/**
* Set editor mode (raw or structured)
* @method setEditorMode
* @param {string} mode - Editor mode
*/
setEditorMode(mode) {
this.state.editorMode = mode;
// Update UI
if (this.elements.editorButtons?.raw && this.elements.editorButtons?.structured) {
this.elements.editorButtons.raw.classList.toggle('active', mode === 'raw');
this.elements.editorButtons.structured.classList.toggle('active', mode === 'structured');
}
if (this.elements.editorContainers?.raw && this.elements.editorContainers?.structured) {
this.elements.editorContainers.raw.style.display = mode === 'raw' ? 'block' : 'none';
this.elements.editorContainers.structured.style.display = mode === 'structured' ? 'block' : 'none';
}
// If switching to structured mode and we have a config file loaded, load it
if (mode === 'structured' && this.state.currentConfigFile && this.structuredEditor) {
this.loadConfigForStructuredEditor();
}
}
/**
* Handle save configuration based on current editor mode
* @method handleSaveConfig
*/
async handleSaveConfig() {
const name = this.state.currentConfigFile;
if (!name) {
alert('Please select a configuration file first.');
return;
}
const restart = this.elements.inputs.restartAfterSave.checked;
if (!confirm(`Save file ${name}? This will create a backup and ${restart ? 'restart' : 'not restart'} networkd.`)) {
return;
}
try {
let content;
if (this.state.editorMode === 'raw') {
content = this.elements.inputs.cfgEditor.value;
} else if (this.structuredEditor) {
content = this.structuredEditor.getConfigurationText();
} else {
throw new Error('Structured editor not available');
}
const result = await this.apiClient.post('/api/save', { name, content, restart });
alert(`Saved: ${result.status ?? 'ok'}`);
// Refresh the config in structured editor if needed
if (this.state.editorMode === 'structured' && this.structuredEditor) {
await this.structuredEditor.loadConfiguration(content, name);
}
} catch (error) {
alert(`Save failed: ${error.message}`);
}
}
/**
* Show specified panel and hide others
* @method show
* @param {string} panel - Panel to show
*/
show(panel) {
// Hide all panels and remove active class from buttons
Object.values(this.elements.panels).forEach(p => {
if (p) p.classList.remove('active');
});
this.elements.buttons.nav.forEach(btn => {
if (btn) btn.classList.remove('active');
});
// Show selected panel and activate button
const targetPanel = this.elements.panels[panel];
const targetButton = document.querySelector(`[data-panel="${panel}"]`);
if (targetPanel) targetPanel.classList.add('active');
if (targetButton) targetButton.classList.add('active');
// Load panel-specific data
const panelActions = {
status: () => this.loadStatus(),
configs: () => this.configManager.refreshConfigs(),
logs: () => this.loadLogs(),
};
panelActions[panel]?.();
}
/**
* Load and display network status
* @method loadStatus
*/
async loadStatus() {
try {
const data = await this.apiClient.get('/api/status');
this.state.interfaces = data.Interfaces ?? [];
this.interfaceRenderer.renderInterfaceTabs(this.state.interfaces);
// Show first interface by default
if (this.state.interfaces.length > 0 && !this.state.currentInterface) {
this.interfaceRenderer.showInterfaceDetails(this.state.interfaces[0]);
}
} catch (error) {
this.elements.outputs.ifaceDetails.innerHTML =
`Error loading status: ${error.message}
`;
}
}
/**
* Load system logs
* @method loadLogs
*/
async loadLogs() {
try {
const text = await this.apiClient.getText('/api/logs');
this.elements.outputs.logsArea.textContent = text;
} catch (error) {
this.elements.outputs.logsArea.textContent = `Error: ${error.message}`;
}
}
/**
* Restart networkd service
* @method restartNetworkd
*/
async restartNetworkd() {
if (!confirm('Restart systemd-networkd? Active connections may be reset.')) return;
try {
const result = await this.apiClient.post('/api/reload');
this.elements.outputs.cmdResult.textContent = `Success: ${JSON.stringify(result)}`;
} catch (error) {
this.elements.outputs.cmdResult.textContent = `Error: ${error.message}`;
}
}
/**
* Reboot the device
* @method rebootDevice
*/
async rebootDevice() {
if (!confirm('Reboot device now?')) return;
try {
const result = await this.apiClient.post('/api/reboot');
this.elements.outputs.cmdResult.textContent = `Success: ${JSON.stringify(result)}`;
} catch (error) {
this.elements.outputs.cmdResult.textContent = `Error: ${error.message}`;
}
}
}
// Initialize application when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
const elements = {
themeToggle: document.getElementById('themeToggle'),
themeIcon: document.getElementById('themeIcon'),
panels: {
status: document.getElementById('panelStatus'),
configs: document.getElementById('panelConfigs'),
logs: document.getElementById('panelLogs'),
commands: document.getElementById('panelCommands'),
},
buttons: {
nav: document.querySelectorAll('.nav-button'),
refreshStatus: document.getElementById('refreshStatus'),
refreshConfigs: document.getElementById('refreshConfigs'),
saveConfig: document.getElementById('saveConfig'),
validateConfig: document.getElementById('validateConfig'),
refreshLogs: document.getElementById('refreshLogs'),
restartNetworkd: document.getElementById('restartNetworkd'),
rebootDevice: document.getElementById('rebootDevice'),
},
inputs: {
configSelect: document.getElementById('configSelect'),
cfgEditor: document.getElementById('cfgEditor'),
restartAfterSave: document.getElementById('restartAfterSave'),
},
outputs: {
ifaceTabs: document.getElementById('interfaceTabs'),
ifaceDetails: document.getElementById('interfaceDetails'),
validateResult: document.getElementById('validateResult'),
logsArea: document.getElementById('logsArea'),
cmdResult: document.getElementById('cmdResult'),
},
};
const app = new Application(elements);
app.init();
// Make app globally available for debugging
window.app = app;
});
export { Application };