/* jshint esversion: 2024, module: true */
import { ApiClient } from "./api-client.js";
import { ConfigManager } from "./config-manager.js";
import { EditorMode, ValidationState } from "./enums.js";
import { InterfaceRenderer } from "./interface-renderer.js";
import { StructuredEditor } from "./structured-editor.js";
import { ThemeManager } from "./theme-manager.js";
/**
* Main Application Class
* @class Application
*/
class Application {
#elements;
#state;
#themeManager;
#structuredEditor;
/**
* @param {Object} elements - DOM elements
*/
constructor(elements) {
this.#elements = elements;
this.#state = {
currentInterface: null,
interfaces: [],
editorMode: EditorMode.RAW,
currentConfigFile: null,
};
// Initialize modules
this.#themeManager = new ThemeManager(elements);
this.#structuredEditor = null;
// Create editor mode toggle UI
this.#createEditorModeToggle();
}
/**
* Initialize the application
* @method init
*/
init() {
this.#themeManager.init();
this.#setupEventListeners();
this.#loadStatus();
this.#initializeStructuredEditor();
}
/**
* Create editor mode toggle UI
* @private
*/
#createEditorModeToggle() {
const editorToggleHTML = `
`;
// Insert the toggle and containers into the configs panel
const configCard =
this.#elements.panels.configs.querySelector(".card:last-child");
configCard.insertAdjacentHTML("afterbegin", editorToggleHTML);
// Move existing form elements to raw editor container
const rawEditorContainer = document.getElementById("rawEditorContainer");
const formGroups = Array.from(
configCard.querySelectorAll(".form-group, .checkbox-group"),
);
const configActions =
configCard.querySelector(".config-actions") ||
configCard.querySelector("div:has(> #validateConfig)");
formGroups.forEach((group) => {
if (!group.closest(".editor-mode-toggle")) {
rawEditorContainer.appendChild(group);
}
});
// Move config actions if they exist
if (configActions) {
rawEditorContainer.appendChild(configActions);
}
// Update elements reference
this.#elements.editorContainers = {
raw: document.getElementById("rawEditorContainer"),
structured: document.getElementById("structuredEditorContainer"),
};
this.#elements.editorButtons = {
raw: document.getElementById("rawEditorBtn"),
structured: document.getElementById("structuredEditorBtn"),
};
}
/**
* Initialize structured editor
* @private
*/
#initializeStructuredEditor() {
if (this.#elements.editorContainers.structured) {
this.#structuredEditor = new StructuredEditor(
this.#elements.editorContainers.structured,
);
} else {
console.error("Structured editor container not found");
}
}
/**
* Set up all event listeners
* @private
*/
#setupEventListeners() {
// Navigation
this.#elements.buttons.nav.forEach((button) => {
button.addEventListener("click", (event) => {
this.show(event.currentTarget.dataset.panel);
});
});
// Status panel
this.#elements.buttons.refreshStatus?.addEventListener("click", () =>
this.#loadStatus(),
);
// Configs panel - raw editor
this.#elements.buttons.refreshConfigs?.addEventListener("click", () =>
this.#handleRefreshConfigs(),
);
this.#elements.buttons.saveConfig?.addEventListener("click", () =>
this.#handleSaveConfig(),
);
this.#elements.buttons.validateConfig?.addEventListener("click", () =>
this.#handleValidateConfig(),
);
this.#elements.inputs.configSelect?.addEventListener("change", () =>
this.#handleConfigFileChange(),
);
// Editor mode toggle
this.#elements.editorButtons?.raw?.addEventListener("click", () =>
this.#setEditorMode(EditorMode.RAW),
);
this.#elements.editorButtons?.structured?.addEventListener("click", () =>
this.#setEditorMode(EditorMode.STRUCTURED),
);
// Logs panel
this.#elements.buttons.refreshLogs?.addEventListener("click", () =>
this.#loadLogs(),
);
// Commands panel
this.#elements.buttons.restartNetworkd?.addEventListener("click", () =>
this.#restartNetworkd(),
);
this.#elements.buttons.rebootDevice?.addEventListener("click", () =>
this.#rebootDevice(),
);
// Touch support
document.addEventListener("touchstart", this.#handleTouchStart, {
passive: true,
});
}
/**
* Handle touch events for better mobile support
* @private
* @param {TouchEvent} event
*/
#handleTouchStart = (event) => {
// Add visual feedback for touch
if (
event.target.classList.contains("button") ||
event.target.classList.contains("nav-button")
) {
event.target.style.opacity = "0.7";
setTimeout(() => {
event.target.style.opacity = "";
}, 150);
}
};
/**
* Handle refresh configuration files
* @private
*/
async #handleRefreshConfigs() {
try {
const files = await ConfigManager.refreshConfigs();
this.#updateConfigSelect(files);
if (files.length > 0) {
this.#state.currentConfigFile = files[0];
await this.#handleConfigFileChange();
} else {
this.#elements.inputs.cfgEditor.value = "";
this.#state.currentConfigFile = null;
}
} catch (error) {
alert(error.message);
}
}
/**
* Update configuration select element
* @private
* @param {Array} files - File names
*/
#updateConfigSelect(files) {
this.#elements.inputs.configSelect.innerHTML = "";
files.forEach((file) => {
const option = new Option(file, file);
this.#elements.inputs.configSelect.add(option);
});
}
/**
* Handle configuration file change
* @private
*/
async #handleConfigFileChange() {
const name = this.#elements.inputs.configSelect.value;
if (!name) return;
this.#state.currentConfigFile = name;
if (this.#state.editorMode === EditorMode.RAW) {
await this.#loadConfigForRawEditor();
} else {
await this.#loadConfigForStructuredEditor();
}
}
/**
* Load config for raw editor
* @private
*/
async #loadConfigForRawEditor() {
try {
const content = await ConfigManager.loadConfig(
this.#state.currentConfigFile,
);
this.#elements.inputs.cfgEditor.value = content;
this.#clearValidationResult();
} catch (error) {
alert(error.message);
}
}
/**
* Load config for structured editor
* @private
*/
async #loadConfigForStructuredEditor() {
if (!this.#structuredEditor) {
console.error("Structured editor not initialized");
return;
}
try {
const content = await ConfigManager.loadConfig(
this.#state.currentConfigFile,
);
// Parse configuration and get schema
const { NetworkConfiguration } = await import("./systemd-network.js");
const config = NetworkConfiguration.fromSystemdConfiguration(content);
const schema = config.getSchema();
// Load schema into structured editor
this.#structuredEditor.loadSchema(schema, this.#state.currentConfigFile);
// Set up event listeners for structured editor
this.#structuredEditor.on("addSection", (event) =>
this.#handleAddSection(event.detail),
);
this.#structuredEditor.on("removeSection", (event) =>
this.#handleRemoveSection(event.detail),
);
} catch (error) {
alert(`Failed to load config for structured editor: ${error.message}`);
}
}
/**
* Handle add section from structured editor
* @private
* @param {Object} detail - Event detail
*/
#handleAddSection(detail) {
console.log("Add section:", detail);
// TODO: Implement section addition logic
}
/**
* Handle remove section from structured editor
* @private
* @param {Object} detail - Event detail
*/
#handleRemoveSection(detail) {
console.log("Remove section:", detail);
// TODO: Implement section removal logic
}
/**
* Handle validate configuration
* @private
*/
async #handleValidateConfig() {
const name = this.#state.currentConfigFile;
if (!name) {
alert("Please select a configuration file first.");
return;
}
let content;
if (this.#state.editorMode === EditorMode.RAW) {
content = this.#elements.inputs.cfgEditor.value;
} else if (this.#structuredEditor) {
content = this.#structuredEditor.getConfigurationText();
} else {
alert("Structured editor not available");
return;
}
this.#setValidationResult(ValidationState.PENDING);
try {
const result = await ConfigManager.validateConfig(name, content);
if (result.ok) {
this.#setValidationResult(ValidationState.SUCCESS);
} else {
this.#setValidationResult(ValidationState.ERROR, result.output);
}
} catch (error) {
this.#setValidationResult(ValidationState.ERROR, error.message);
}
}
/**
* Set validation result
* @private
* @param {Symbol} state - Validation state
* @param {string} [message] - Additional message
*/
#setValidationResult(state, message = "") {
this.#elements.outputs.validateResult.textContent =
ConfigManager.getValidationMessage(state, message);
this.#elements.outputs.validateResult.className =
ConfigManager.getValidationClass(state);
}
/**
* Clear validation result
* @private
*/
#clearValidationResult() {
this.#elements.outputs.validateResult.textContent = "";
this.#elements.outputs.validateResult.className = "";
}
/**
* Set editor mode (raw or structured)
* @private
* @param {Symbol} mode - Editor mode
*/
#setEditorMode(mode) {
this.#state.editorMode = mode;
// Update UI
if (
this.#elements.editorButtons?.raw &&
this.#elements.editorButtons?.structured
) {
this.#elements.editorButtons.raw.classList.toggle(
"active",
mode === EditorMode.RAW,
);
this.#elements.editorButtons.structured.classList.toggle(
"active",
mode === EditorMode.STRUCTURED,
);
}
if (
this.#elements.editorContainers?.raw &&
this.#elements.editorContainers?.structured
) {
this.#elements.editorContainers.raw.style.display =
mode === EditorMode.RAW ? "block" : "none";
this.#elements.editorContainers.structured.style.display =
mode === EditorMode.STRUCTURED ? "block" : "none";
}
// If switching to structured mode and we have a config file loaded, load it
if (
mode === EditorMode.STRUCTURED &&
this.#state.currentConfigFile &&
this.#structuredEditor
) {
this.#loadConfigForStructuredEditor();
}
}
/**
* Handle save configuration based on current editor mode
* @private
*/
async #handleSaveConfig() {
const name = this.#state.currentConfigFile;
if (!name) {
alert("Please select a configuration file first.");
return;
}
const restart = this.#elements.inputs.restartAfterSave.checked;
if (
!confirm(
`Save file ${name}? This will create a backup and ${restart ? "restart" : "not restart"} networkd.`,
)
) {
return;
}
try {
let content;
if (this.#state.editorMode === EditorMode.RAW) {
content = this.#elements.inputs.cfgEditor.value;
} else if (this.#structuredEditor) {
content = this.#structuredEditor.getConfigurationText();
} else {
throw new Error("Structured editor not available");
}
const result = await ConfigManager.saveConfig(name, content, restart);
alert(`Saved: ${result.status ?? "ok"}`);
// Refresh the config in structured editor if needed
if (
this.#state.editorMode === EditorMode.STRUCTURED &&
this.#structuredEditor
) {
await this.#loadConfigForStructuredEditor();
}
} catch (error) {
alert(`Save failed: ${error.message}`);
}
}
/**
* Load and display network status
* @private
*/
async #loadStatus() {
try {
const data = await ApiClient.getNetworkStatus();
this.#state.interfaces = data.Interfaces ?? [];
// Render interface tabs
const tabsHTML = InterfaceRenderer.renderInterfaceTabs(
this.#state.interfaces,
this.#state.currentInterface,
);
this.#elements.outputs.ifaceTabs.innerHTML = tabsHTML;
// Show first interface by default
if (this.#state.interfaces.length > 0 && !this.#state.currentInterface) {
this.#showInterfaceDetails(this.#state.interfaces[0]);
}
// Add event listeners to tabs
this.#elements.outputs.ifaceTabs
.querySelectorAll(".interface-tab")
.forEach((tab) => {
tab.addEventListener("click", (event) => {
const ifaceName = event.currentTarget.dataset.interface;
const iface = this.#state.interfaces.find(
(i) => i.Name === ifaceName,
);
if (iface) {
this.#showInterfaceDetails(iface);
}
});
});
} catch (error) {
this.#elements.outputs.ifaceDetails.innerHTML = `Error loading status: ${error.message}
`;
}
}
/**
* Show interface details
* @private
* @param {Object} iface - Interface object
*/
#showInterfaceDetails(iface) {
this.#state.currentInterface = iface;
// Update active tab
this.#elements.outputs.ifaceTabs
.querySelectorAll(".interface-tab")
.forEach((tab) => {
tab.classList.toggle("active", tab.dataset.interface === iface.Name);
});
const detailsHTML = InterfaceRenderer.showInterfaceDetails(iface);
this.#elements.outputs.ifaceDetails.innerHTML = detailsHTML;
}
/**
* Load system logs
* @private
*/
async #loadLogs() {
try {
const text = await ApiClient.getSystemLogs();
this.#elements.outputs.logsArea.textContent = text;
} catch (error) {
this.#elements.outputs.logsArea.textContent = `Error: ${error.message}`;
}
}
/**
* Restart networkd service
* @private
*/
async #restartNetworkd() {
if (!confirm("Restart systemd-networkd? Active connections may be reset."))
return;
try {
const result = await ApiClient.restartNetworkd();
this.#elements.outputs.cmdResult.textContent = `Success: ${JSON.stringify(result)}`;
} catch (error) {
this.#elements.outputs.cmdResult.textContent = `Error: ${error.message}`;
}
}
/**
* Reboot the device
* @private
*/
async #rebootDevice() {
if (!confirm("Reboot device now?")) return;
try {
const result = await ApiClient.rebootDevice();
this.#elements.outputs.cmdResult.textContent = `Success: ${JSON.stringify(result)}`;
} catch (error) {
this.#elements.outputs.cmdResult.textContent = `Error: ${error.message}`;
}
}
/**
* Show specified panel and hide others
* @method show
* @param {string} panel - Panel to show
*/
show(panel) {
// Hide all panels and remove active class from buttons
Object.values(this.#elements.panels).forEach((p) => {
if (p) p.classList.remove("active");
});
this.#elements.buttons.nav.forEach((btn) => {
if (btn) btn.classList.remove("active");
});
// Show selected panel and activate button
const targetPanel = this.#elements.panels[panel];
const targetButton = document.querySelector(`[data-panel="${panel}"]`);
if (targetPanel) targetPanel.classList.add("active");
if (targetButton) targetButton.classList.add("active");
// Load panel-specific data
const panelActions = {
status: () => this.#loadStatus(),
configs: () => this.#handleRefreshConfigs(),
logs: () => this.#loadLogs(),
};
panelActions[panel]?.();
}
/**
* Get current application state (for debugging)
* @method getState
* @returns {Object} Application state
*/
getState() {
return { ...this.#state };
}
/**
* Get theme manager instance
* @method getThemeManager
* @returns {ThemeManager} Theme manager instance
*/
getThemeManager() {
return this.#themeManager;
}
}
// Initialize application when DOM is loaded
document.addEventListener("DOMContentLoaded", () => {
const elements = {
themeToggle: document.getElementById("themeToggle"),
themeIcon: document.getElementById("themeIcon"),
panels: {
status: document.getElementById("panelStatus"),
configs: document.getElementById("panelConfigs"),
logs: document.getElementById("panelLogs"),
commands: document.getElementById("panelCommands"),
},
buttons: {
nav: document.querySelectorAll(".nav-button"),
refreshStatus: document.getElementById("refreshStatus"),
refreshConfigs: document.getElementById("refreshConfigs"),
saveConfig: document.getElementById("saveConfig"),
validateConfig: document.getElementById("validateConfig"),
refreshLogs: document.getElementById("refreshLogs"),
restartNetworkd: document.getElementById("restartNetworkd"),
rebootDevice: document.getElementById("rebootDevice"),
},
inputs: {
configSelect: document.getElementById("configSelect"),
cfgEditor: document.getElementById("cfgEditor"),
restartAfterSave: document.getElementById("restartAfterSave"),
},
outputs: {
ifaceTabs: document.getElementById("interfaceTabs"),
ifaceDetails: document.getElementById("interfaceDetails"),
validateResult: document.getElementById("validateResult"),
logsArea: document.getElementById("logsArea"),
cmdResult: document.getElementById("cmdResult"),
},
};
const app = new Application(elements);
app.init();
// Make app globally available for debugging
window.app = app;
});
export { Application };