summaryrefslogtreecommitdiffstats
path: root/static/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'static/app.js')
-rw-r--r--static/app.js886
1 files changed, 230 insertions, 656 deletions
diff --git a/static/app.js b/static/app.js
index 54c1681..8a04c45 100644
--- a/static/app.js
+++ b/static/app.js
@@ -1,660 +1,234 @@
/* jshint esversion: 2024, module: true */
-// import { StructuredEditor } from "./structured-editor.js";
-
-/**
- * Network Classroom Web UI - Modern ES2024 Implementation
- * @module NetworkUI
- */
-
-// Global state
-const state = {
- currentInterface: null,
- interfaces: [],
- theme: localStorage.getItem("network-ui-theme") || "dark",
- structuredEditor: null,
- editorMode: "raw",
-};
-
-/**
- * DOM Elements cache
- * @type {Object}
- */
-const elements = Object.freeze({
- themeToggle: document.getElementById("themeToggle"),
- themeIcon: document.getElementById("themeIcon"),
- panels: Object.freeze({
- status: document.getElementById("panelStatus"),
- configs: document.getElementById("panelConfigs"),
- logs: document.getElementById("panelLogs"),
- commands: document.getElementById("panelCommands"),
- }),
- buttons: Object.freeze({
- 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: Object.freeze({
- configSelect: document.getElementById("configSelect"),
- cfgEditor: document.getElementById("cfgEditor"),
- restartAfterSave: document.getElementById("restartAfterSave"),
- }),
- outputs: Object.freeze({
- ifaceTabs: document.getElementById("interfaceTabs"),
- ifaceDetails: document.getElementById("interfaceDetails"),
- validateResult: document.getElementById("validateResult"),
- logsArea: document.getElementById("logsArea"),
- cmdResult: document.getElementById("cmdResult"),
- }),
+import { ApiClient } from "./api-client.js";
+import { ConfigManager } from "./config-manager.js";
+import { InterfaceRenderer } from "./interface-renderer.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: "raw",
+ };
+
+ // 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,
+ );
+ }
+
+ /**
+ * Initialize the application
+ * @method init
+ */
+ init() {
+ this.themeManager.init();
+ this.setupEventListeners();
+ this.loadStatus();
+ }
+
+ /**
+ * 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 - delegated to ConfigManager
+ this.configManager.setupEventListeners();
+
+ // 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);
+ }
+ };
+
+ /**
+ * 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) =>
+ p?.classList.remove("active"),
+ );
+ this.elements.buttons.nav.forEach((btn) => btn?.classList.remove("active"));
+
+ // Show selected panel and activate button
+ this.elements.panels[panel]?.classList.add("active");
+ document.querySelector(`[data-panel="${panel}"]`)?.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 = `<div class="error-message">Error loading status: ${error.message}</div>`;
+ }
+ }
+
+ /**
+ * 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;
});
-/**
- * Initialize the application
- * @function init
- */
-const init = () => {
- applyTheme(state.theme);
- setupEventListeners();
- loadStatus();
-};
-
-/**
- * Toggle between light and dark themes
- * @function toggleTheme
- */
-const toggleTheme = () => {
- const newTheme = state.theme === "dark" ? "light" : "dark";
- applyTheme(newTheme);
-};
-
-/**
- * Apply theme to document
- * @function applyTheme
- * @param {string} theme - Theme name ('light' or 'dark')
- */
-const applyTheme = (theme) => {
- document.documentElement.setAttribute("data-theme", theme);
- state.theme = theme;
- localStorage.setItem("network-ui-theme", theme);
-
- // Update theme icon
- if (elements.themeIcon) {
- elements.themeIcon.textContent = theme === "dark" ? "☀️" : "🌙";
- }
-};
-
-/**
- * Set up all event listeners
- * @function setupEventListeners
- */
-const setupEventListeners = () => {
- // Navigation
- elements.themeToggle?.addEventListener("click", toggleTheme);
- elements.buttons.nav.forEach((button) => {
- button.addEventListener("click", (event) => {
- show(event.currentTarget.dataset.panel);
- });
- });
-
- // Status panel
- elements.buttons.refreshStatus?.addEventListener("click", loadStatus);
-
- // Configs panel
- elements.buttons.refreshConfigs?.addEventListener("click", refreshConfigs);
- elements.buttons.saveConfig?.addEventListener("click", saveConfig);
- elements.buttons.validateConfig?.addEventListener("click", validateConfig);
- elements.inputs.configSelect?.addEventListener("change", loadConfig);
-
- // Logs panel
- elements.buttons.refreshLogs?.addEventListener("click", loadLogs);
-
- // Commands panel
- elements.buttons.restartNetworkd?.addEventListener("click", restartNetworkd);
- elements.buttons.rebootDevice?.addEventListener("click", rebootDevice);
-
- // Touch support
- document.addEventListener("touchstart", handleTouchStart, { passive: true });
-};
-
-/**
- * Handle touch events for better mobile support
- * @function handleTouchStart
- * @param {TouchEvent} event
- */
-const 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);
- }
-};
-
-/**
- * Show specified panel and hide others
- * @function show
- * @param {string} panel - Panel to show
- */
-const show = (panel) => {
- // Hide all panels and remove active class from buttons
- Object.values(elements.panels).forEach((p) => p?.classList.remove("active"));
- elements.buttons.nav.forEach((btn) => btn?.classList.remove("active"));
-
- // Show selected panel and activate button
- elements.panels[panel]?.classList.add("active");
- document.querySelector(`[data-panel="${panel}"]`)?.classList.add("active");
-
- // Load panel-specific data
- const panelActions = {
- status: loadStatus,
- configs: refreshConfigs,
- logs: loadLogs,
- };
-
- panelActions[panel]?.();
-};
-
-/**
- * API utility function
- * @function api
- * @param {string} path - API endpoint
- * @param {Object} [options] - Fetch options
- * @returns {Promise<Response>}
- */
-const api = async (path, options = {}) => {
- const response = await fetch(path, options);
-
- if (!response.ok) {
- const text = await response.text();
- throw new Error(`${response.status} ${text}`);
- }
-
- return response;
-};
-
-/**
- * Load and display network status
- * @function loadStatus
- */
-const loadStatus = async () => {
- try {
- const response = await api("/api/status");
- const data = await response.json();
- state.interfaces = data.Interfaces ?? [];
- renderInterfaceTabs(state.interfaces);
-
- // Show first interface by default
- if (state.interfaces.length > 0 && !state.currentInterface) {
- showInterfaceDetails(state.interfaces[0]);
- }
- } catch (error) {
- elements.outputs.ifaceDetails.innerHTML = `<div class="error-message">Error loading status: ${error.message}</div>`;
- }
-};
-
-/**
- * Render interface tabs
- * @function renderInterfaceTabs
- * @param {Array} interfaces - Array of interface objects
- */
-const renderInterfaceTabs = (interfaces) => {
- if (!interfaces.length) {
- elements.outputs.ifaceTabs.innerHTML =
- '<div class="no-interfaces">No network interfaces found</div>';
- elements.outputs.ifaceDetails.innerHTML = "";
- return;
- }
-
- const tabsHTML = interfaces
- .map(
- (iface) => `
- <button class="interface-tab ${iface === state.currentInterface ? "active" : ""}"
- data-interface="${iface.Name}">
- ${iface.Name}
- <span class="interface-state ${getStateClass(iface)}">${getStateText(iface)}</span>
- </button>
- `,
- )
- .join("");
-
- elements.outputs.ifaceTabs.innerHTML = `<div class="interface-tabs-container">${tabsHTML}</div>`;
-
- // Add event listeners to tabs
- elements.outputs.ifaceTabs
- .querySelectorAll(".interface-tab")
- .forEach((tab) => {
- tab.addEventListener("click", (event) => {
- const ifaceName = event.currentTarget.dataset.interface;
- const iface = interfaces.find((i) => i.Name === ifaceName);
- if (iface) {
- showInterfaceDetails(iface);
- }
- });
- });
-};
-
-/**
- * Show detailed interface information with abbreviations
- * @function showInterfaceDetails
- * @param {Object} iface - Interface object
- */
-const showInterfaceDetails = (iface) => {
- state.currentInterface = iface;
-
- // Update active tab
- elements.outputs.ifaceTabs
- .querySelectorAll(".interface-tab")
- .forEach((tab) => {
- tab.classList.toggle("active", tab.dataset.interface === iface.Name);
- });
-
- const detailsHTML = `
- <div class="interface-detail-grid">
- ${renderDetailRow("Link File", iface.LinkFile)}
- ${renderDetailRow("Network File", iface.NetworkFile)}
- ${renderDetailRow("State", iface.State, getStateClass(iface))}
- ${renderDetailRow("Online State", iface.OnlineState)}
- ${renderDetailRow("Type", iface.Type)}
- ${renderDetailRow("Path", iface.Path)}
- ${renderDetailRow("Driver", iface.Driver)}
- ${renderDetailRow("Vendor", iface.Vendor)}
- ${renderDetailRow("Model", iface.Model)}
- ${renderDetailRow("Hardware Address", arrayToMac(iface.HardwareAddress))}
- ${renderDetailRow("MTU", iface.MTU ? `${iface.MTU} (min: ${iface.MTUMin ?? "?"}, max: ${iface.MTUMax ?? "?"})` : "")}
- ${renderDetailRow("QDisc", iface.QDisc)}
- ${renderDetailRow("IPv6 Address Generation Mode", iface.IPv6AddressGenerationMode)}
- ${renderDetailRow("Number of Queues (Tx/Rx)", iface.Queues ? `${iface.Queues.Tx ?? "?"}/${iface.Queues.Rx ?? "?"}` : "")}
- ${renderDetailRow("Auto negotiation", iface.AutoNegotiation ? "yes" : "no")}
- ${renderDetailRow("Speed", iface.Speed)}
- ${renderDetailRow("Duplex", iface.Duplex)}
- ${renderDetailRow("Port", iface.Port)}
- ${renderDetailRow("Address", renderAddressList(iface.Addresses))}
- ${renderDetailRow("DNS", renderDNSServerList(iface.DNS))}
- ${renderDetailRow("NTP", iface.NTP)}
- ${renderDetailRow("Activation Policy", iface.ActivationPolicy)}
- ${renderDetailRow("Required For Online", iface.RequiredForOnline ? "yes" : "no")}
- ${renderDetailRow("Connected To", iface.ConnectedTo)}
- ${renderDetailRow("Offered DHCP leases", renderDHCPLeases(iface.DHCPLeases))}
- </div>
- `;
-
- elements.outputs.ifaceDetails.innerHTML = detailsHTML;
-};
-
-/**
- * Render a detail row with abbreviations
- * @function renderDetailRow
- * @param {string} label - Row label
- * @param {string} value - Row value
- * @param {string} [valueClass] - CSS class for value
- * @returns {string} HTML string
- */
-const renderDetailRow = (label, value, valueClass = "") => {
- if (!value) return "";
-
- // Add abbreviations for common networking terms
- 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}">${value}</span>
- </div>
- `;
-};
-
-/**
- * Render address list
- * @function renderAddressList
- * @param {Array} addresses - Array of addresses
- * @returns {string} Formatted addresses
- */
-const renderAddressList = (addresses) => {
- if (!addresses?.length) return "";
-
- return addresses
- .map((addr) => {
- const ip = ipFromArray(addr);
- return ip ? `<div class="address-item">${ip}</div>` : "";
- })
- .join("");
-};
-
-/**
- * Render DNS server list
- * @function renderDNSServerList
- * @param {Array} dnsServers - Array of DNS servers
- * @returns {string} Formatted DNS servers
- */
-const renderDNSServerList = (dnsServers) => {
- if (!dnsServers?.length) return "";
-
- return dnsServers
- .map((dns) => {
- const server = ipFromArray(dns.Address ?? dns);
- return server ? `<div class="dns-item">${server}</div>` : "";
- })
- .join("");
-};
-
-/**
- * Render DHCP leases
- * @function renderDHCPLeases
- * @param {Array} leases - Array of DHCP leases
- * @returns {string} Formatted leases
- */
-const 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">${ip} (to ${to})</div>`;
- })
- .join("");
-};
-
-/**
- * Get CSS class for interface state
- * @function getStateClass
- * @param {Object} iface - Interface object
- * @returns {string} CSS class
- */
-const getStateClass = (iface) => {
- const state =
- iface.OperationalState ?? iface.AdministrativeState ?? iface.State ?? "";
- return state.toLowerCase().includes("up") ||
- state.toLowerCase().includes("routable") ||
- state.toLowerCase().includes("configured")
- ? "state-up"
- : "state-down";
-};
-
-/**
- * Get display text for interface state
- * @function getStateText
- * @param {Object} iface - Interface object
- * @returns {string} State text
- */
-const getStateText = (iface) => {
- return (
- iface.OperationalState ??
- iface.AdministrativeState ??
- iface.State ??
- "unknown"
- );
-};
-
-/**
- * Convert byte array to MAC address
- * @function arrayToMac
- * @param {Array} bytes - Byte array
- * @returns {string} MAC address
- */
-const arrayToMac = (bytes) => {
- if (!Array.isArray(bytes)) return "";
-
- return bytes.map((byte) => byte.toString(16).padStart(2, "0")).join(":");
-};
-
-/**
- * Convert byte array to IP address
- * @function ipFromArray
- * @param {Array|Object} obj - IP data
- * @returns {string} IP address
- */
-const 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
- * @function routeToString
- * @param {Object} route - Route object
- * @returns {string} Route string
- */
-const routeToString = (route) => {
- if (!route) return "";
-
- const destination = route.Destination
- ? ipFromArray(route.Destination)
- : "default";
- const gateway = route.Gateway ? ipFromArray(route.Gateway) : "";
-
- return gateway ? `${destination} → ${gateway}` : destination;
-};
-
-/**
- * Refresh configuration file list
- * @function refreshConfigs
- */
-const refreshConfigs = async () => {
- try {
- const response = await api("/api/configs");
- const data = await response.json();
-
- elements.inputs.configSelect.innerHTML = "";
- data.files?.forEach((file) => {
- const option = new Option(file, file);
- elements.inputs.configSelect.add(option);
- });
-
- if (data.files?.length > 0) {
- await loadConfig();
- } else {
- elements.inputs.cfgEditor.value = "";
- }
- } catch (error) {
- alert(`Failed to list configs: ${error.message}`);
- }
-};
-
-/**
- * Load selected configuration file
- * @function loadConfig
- */
-const loadConfig = async () => {
- const name = elements.inputs.configSelect.value;
- if (!name) return;
-
- try {
- const response = await api(`/api/config/${encodeURIComponent(name)}`);
- const text = await response.text();
- elements.inputs.cfgEditor.value = text;
- elements.outputs.validateResult.textContent = "";
- } catch (error) {
- alert(`Failed to load: ${error.message}`);
- }
-};
-
-/**
- * Validate current configuration
- * @function validateConfig
- */
-const validateConfig = async () => {
- const name = elements.inputs.configSelect.value;
- const content = elements.inputs.cfgEditor.value;
-
- elements.outputs.validateResult.textContent = "Validating...";
- elements.outputs.validateResult.className = "validation-pending";
-
- try {
- const response = await api("/api/validate", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ name, content }),
- });
-
- const result = await response.json();
-
- if (result.ok) {
- elements.outputs.validateResult.textContent = "✓ Configuration is valid";
- elements.outputs.validateResult.className = "validation-success";
- } else {
- elements.outputs.validateResult.textContent = `✗ ${result.output || "Validation failed"}`;
- elements.outputs.validateResult.className = "validation-error";
- }
- } catch (error) {
- elements.outputs.validateResult.textContent = `✗ Error: ${error.message}`;
- elements.outputs.validateResult.className = "validation-error";
- }
-};
-
-/**
- * Save current configuration
- * @function saveConfig
- */
-const saveConfig = async () => {
- const name = elements.inputs.configSelect.value;
- const content = elements.inputs.cfgEditor.value;
- const restart = elements.inputs.restartAfterSave.checked;
-
- if (
- !confirm(
- `Save file ${name}? This will create a backup and ${restart ? "restart" : "not restart"} networkd.`,
- )
- ) {
- return;
- }
-
- try {
- const response = await api("/api/save", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ name, content, restart }),
- });
-
- const result = await response.json();
- alert(`Saved: ${result.status ?? "ok"}`);
- } catch (error) {
- alert(`Save failed: ${error.message}`);
- }
-};
-
-/**
- * Load system logs
- * @function loadLogs
- */
-const loadLogs = async () => {
- try {
- const response = await api("/api/logs");
- const text = await response.text();
- elements.outputs.logsArea.textContent = text;
- } catch (error) {
- elements.outputs.logsArea.textContent = `Error: ${error.message}`;
- }
-};
-
-/**
- * Restart networkd service
- * @function restartNetworkd
- */
-const restartNetworkd = async () => {
- if (!confirm("Restart systemd-networkd? Active connections may be reset."))
- return;
-
- try {
- const response = await api("/api/reload", { method: "POST" });
- const result = await response.json();
- elements.outputs.cmdResult.textContent = `Success: ${JSON.stringify(result)}`;
- } catch (error) {
- elements.outputs.cmdResult.textContent = `Error: ${error.message}`;
- }
-};
-
-/**
- * Reboot the device
- * @function rebootDevice
- */
-const rebootDevice = async () => {
- if (!confirm("Reboot device now?")) return;
-
- try {
- const response = await api("/api/reboot", { method: "POST" });
- const result = await response.json();
- elements.outputs.cmdResult.textContent = `Success: ${JSON.stringify(result)}`;
- } catch (error) {
- elements.outputs.cmdResult.textContent = `Error: ${error.message}`;
- }
-};
-
-// Initialize structured editor when needed
-const initStructuredEditor = () => {
- if (!state.structuredEditor) {
- state.structuredEditor = new StructuredEditor(
- document.getElementById("structuredEditorContainer"),
- );
- }
-};
-
-// Initialize when DOM is loaded
-document.addEventListener("DOMContentLoaded", init);
-
-// Export for module usage
-export {
- show,
- api,
- loadStatus,
- refreshConfigs,
- loadConfig,
- validateConfig,
- saveConfig,
- loadLogs,
- restartNetworkd,
- rebootDevice,
- arrayToMac,
- ipFromArray,
- routeToString,
-};
+export { Application };