diff --git a/.gitignore b/.gitignore
index 78a92910..f0dcf29e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,6 @@
**/TODO
/yggdrasil
/yggdrasilctl
-/yggdrasil.conf
-/yggdrasil.json
+/yggdrasil.*
/run
/test
\ No newline at end of file
diff --git a/cmd/yggdrasil/main.go b/cmd/yggdrasil/main.go
index 36eedb41..19cfcfb1 100644
--- a/cmd/yggdrasil/main.go
+++ b/cmd/yggdrasil/main.go
@@ -107,6 +107,7 @@ func main() {
}
cfg := config.GenerateConfig()
+ var configPath string
var err error
switch {
case *ver:
@@ -124,6 +125,7 @@ func main() {
}
case *useconffile != "":
+ configPath = *useconffile
f, err := os.Open(*useconffile)
if err != nil {
panic(err)
@@ -206,6 +208,9 @@ func main() {
return
}
+ // Set current config for web UI
+ config.SetCurrentConfig(configPath, cfg)
+
n := &node{}
// Set up the Yggdrasil node itself.
diff --git a/src/config/config.go b/src/config/config.go
index 0358da8b..09946ec1 100644
--- a/src/config/config.go
+++ b/src/config/config.go
@@ -30,6 +30,7 @@ import (
"io"
"math/big"
"os"
+ "path/filepath"
"time"
"github.com/hjson/hjson-go/v4"
@@ -274,3 +275,190 @@ func (k *KeyBytes) UnmarshalJSON(b []byte) error {
*k, err = hex.DecodeString(s)
return err
}
+
+// ConfigInfo contains information about the configuration file
+type ConfigInfo struct {
+ Path string `json:"path"`
+ Format string `json:"format"`
+ Data interface{} `json:"data"`
+ Writable bool `json:"writable"`
+}
+
+// Global variables to track the current configuration state
+var (
+ currentConfigPath string
+ currentConfigData *NodeConfig
+)
+
+// SetCurrentConfig sets the current configuration data and path
+func SetCurrentConfig(path string, cfg *NodeConfig) {
+ currentConfigPath = path
+ currentConfigData = cfg
+}
+
+// GetCurrentConfig returns the current configuration information
+func GetCurrentConfig() (*ConfigInfo, error) {
+ var configPath string
+ var configData *NodeConfig
+ var format string = "hjson"
+ var writable bool = false
+
+ // Use current config if available, otherwise try to read from default location
+ if currentConfigPath != "" && currentConfigData != nil {
+ configPath = currentConfigPath
+ configData = currentConfigData
+ } else {
+ // Fallback to default path
+ defaults := GetDefaults()
+ configPath = defaults.DefaultConfigFile
+
+ // Try to read existing config file
+ if _, err := os.Stat(configPath); err == nil {
+ data, err := os.ReadFile(configPath)
+ if err == nil {
+ cfg := GenerateConfig()
+ if err := hjson.Unmarshal(data, cfg); err == nil {
+ configData = cfg
+ // Detect format
+ var jsonTest interface{}
+ if json.Unmarshal(data, &jsonTest) == nil {
+ format = "json"
+ }
+ } else {
+ return nil, fmt.Errorf("failed to parse config file: %v", err)
+ }
+ }
+ } else {
+ // No config file exists, use default
+ configData = GenerateConfig()
+ }
+ }
+
+ // Detect format from file if path is known
+ if configPath != "" {
+ if _, err := os.Stat(configPath); err == nil {
+ data, err := os.ReadFile(configPath)
+ if err == nil {
+ var jsonTest interface{}
+ if json.Unmarshal(data, &jsonTest) == nil {
+ format = "json"
+ }
+ }
+ }
+ }
+
+ // Check if writable
+ if configPath != "" {
+ if _, err := os.Stat(configPath); err == nil {
+ // File exists, check if writable
+ if file, err := os.OpenFile(configPath, os.O_WRONLY, 0); err == nil {
+ writable = true
+ file.Close()
+ }
+ } else {
+ // File doesn't exist, check if directory is writable
+ dir := filepath.Dir(configPath)
+ if stat, err := os.Stat(dir); err == nil && stat.IsDir() {
+ testFile := filepath.Join(dir, ".yggdrasil_write_test")
+ if file, err := os.Create(testFile); err == nil {
+ file.Close()
+ os.Remove(testFile)
+ writable = true
+ }
+ }
+ }
+ }
+
+ return &ConfigInfo{
+ Path: configPath,
+ Format: format,
+ Data: configData,
+ Writable: writable,
+ }, nil
+}
+
+// SaveConfig saves configuration to file
+func SaveConfig(configData interface{}, configPath, format string) error {
+ // Validate config data
+ var testConfig NodeConfig
+ configBytes, err := json.Marshal(configData)
+ if err != nil {
+ return fmt.Errorf("failed to marshal config data: %v", err)
+ }
+
+ if err := json.Unmarshal(configBytes, &testConfig); err != nil {
+ return fmt.Errorf("invalid configuration data: %v", err)
+ }
+
+ // Determine target path
+ targetPath := configPath
+ if targetPath == "" {
+ if currentConfigPath != "" {
+ targetPath = currentConfigPath
+ } else {
+ defaults := GetDefaults()
+ targetPath = defaults.DefaultConfigFile
+ }
+ }
+
+ // Determine format if not specified
+ targetFormat := format
+ if targetFormat == "" {
+ if _, err := os.Stat(targetPath); err == nil {
+ data, readErr := os.ReadFile(targetPath)
+ if readErr == nil {
+ var jsonTest interface{}
+ if json.Unmarshal(data, &jsonTest) == nil {
+ targetFormat = "json"
+ } else {
+ targetFormat = "hjson"
+ }
+ }
+ }
+ if targetFormat == "" {
+ targetFormat = "hjson"
+ }
+ }
+
+ // Create backup if file exists
+ if _, err := os.Stat(targetPath); err == nil {
+ backupPath := targetPath + ".backup"
+ if data, err := os.ReadFile(targetPath); err == nil {
+ if err := os.WriteFile(backupPath, data, 0600); err != nil {
+ return fmt.Errorf("failed to create backup: %v", err)
+ }
+ }
+ }
+
+ // Ensure directory exists
+ dir := filepath.Dir(targetPath)
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return fmt.Errorf("failed to create config directory: %v", err)
+ }
+
+ // Marshal to target format
+ var outputData []byte
+ if targetFormat == "json" {
+ outputData, err = json.MarshalIndent(configData, "", " ")
+ if err != nil {
+ return fmt.Errorf("failed to marshal JSON: %v", err)
+ }
+ } else {
+ outputData, err = hjson.Marshal(configData)
+ if err != nil {
+ return fmt.Errorf("failed to marshal HJSON: %v", err)
+ }
+ }
+
+ // Write file
+ if err := os.WriteFile(targetPath, outputData, 0600); err != nil {
+ return fmt.Errorf("failed to write config file: %v", err)
+ }
+
+ // Update current config if this is the current config file
+ if targetPath == currentConfigPath {
+ currentConfigData = &testConfig
+ }
+
+ return nil
+}
diff --git a/src/webui/server.go b/src/webui/server.go
index be480dc4..0848d101 100644
--- a/src/webui/server.go
+++ b/src/webui/server.go
@@ -13,6 +13,7 @@ import (
"time"
"github.com/yggdrasil-network/yggdrasil-go/src/admin"
+ "github.com/yggdrasil-network/yggdrasil-go/src/config"
"github.com/yggdrasil-network/yggdrasil-go/src/core"
)
@@ -381,6 +382,105 @@ func (w *WebUIServer) callAdminHandler(command string, args map[string]interface
return w.admin.CallHandler(command, argsBytes)
}
+// Configuration response structures
+type ConfigResponse struct {
+ ConfigPath string `json:"config_path"`
+ ConfigFormat string `json:"config_format"`
+ ConfigData interface{} `json:"config_data"`
+ IsWritable bool `json:"is_writable"`
+}
+
+type ConfigSetRequest struct {
+ ConfigData interface{} `json:"config_data"`
+ ConfigPath string `json:"config_path,omitempty"`
+ Format string `json:"format,omitempty"`
+}
+
+type ConfigSetResponse struct {
+ Success bool `json:"success"`
+ Message string `json:"message"`
+ ConfigPath string `json:"config_path"`
+ BackupPath string `json:"backup_path,omitempty"`
+}
+
+// getConfigHandler handles configuration file reading
+func (w *WebUIServer) getConfigHandler(rw http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ // Use config package to get current configuration
+ configInfo, err := config.GetCurrentConfig()
+ if err != nil {
+ w.log.Errorf("Failed to get current config: %v", err)
+ http.Error(rw, "Failed to get configuration", http.StatusInternalServerError)
+ return
+ }
+
+ response := ConfigResponse{
+ ConfigPath: configInfo.Path,
+ ConfigFormat: configInfo.Format,
+ ConfigData: configInfo.Data,
+ IsWritable: configInfo.Writable,
+ }
+
+ rw.Header().Set("Content-Type", "application/json")
+ if err := json.NewEncoder(rw).Encode(response); err != nil {
+ w.log.Errorf("Failed to encode config response: %v", err)
+ http.Error(rw, "Failed to encode response", http.StatusInternalServerError)
+ }
+}
+
+// setConfigHandler handles configuration file writing
+func (w *WebUIServer) setConfigHandler(rw http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ var req ConfigSetRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(rw, "Invalid request body", http.StatusBadRequest)
+ return
+ }
+
+ // Use config package to save configuration
+ err := config.SaveConfig(req.ConfigData, req.ConfigPath, req.Format)
+ if err != nil {
+ response := ConfigSetResponse{
+ Success: false,
+ Message: err.Error(),
+ }
+ w.writeJSONResponse(rw, response)
+ return
+ }
+
+ // Get current config info for response
+ configInfo, err := config.GetCurrentConfig()
+ var configPath string = req.ConfigPath
+ if err == nil && configInfo != nil {
+ configPath = configInfo.Path
+ }
+
+ response := ConfigSetResponse{
+ Success: true,
+ Message: "Configuration saved successfully",
+ ConfigPath: configPath,
+ BackupPath: configPath + ".backup",
+ }
+ w.writeJSONResponse(rw, response)
+}
+
+// writeJSONResponse helper function
+func (w *WebUIServer) writeJSONResponse(rw http.ResponseWriter, data interface{}) {
+ rw.Header().Set("Content-Type", "application/json")
+ if err := json.NewEncoder(rw).Encode(data); err != nil {
+ w.log.Errorf("Failed to encode JSON response: %v", err)
+ http.Error(rw, "Failed to encode response", http.StatusInternalServerError)
+ }
+}
+
func (w *WebUIServer) Start() error {
// Validate listen address before starting
if w.listen != "" {
@@ -415,6 +515,10 @@ func (w *WebUIServer) Start() error {
// Admin API endpoints - with auth
mux.HandleFunc("/api/admin/", w.authMiddleware(w.adminAPIHandler))
+ // Configuration API endpoints - with auth
+ mux.HandleFunc("/api/config/get", w.authMiddleware(w.getConfigHandler))
+ mux.HandleFunc("/api/config/set", w.authMiddleware(w.setConfigHandler))
+
// Setup static files handler (implementation varies by build)
setupStaticHandler(mux, w)
diff --git a/src/webui/static/config.js b/src/webui/static/config.js
new file mode 100644
index 00000000..28e8d0f5
--- /dev/null
+++ b/src/webui/static/config.js
@@ -0,0 +1,411 @@
+// Configuration management functions
+
+let currentConfig = null;
+let configMeta = null;
+
+// Initialize config section
+async function initConfigSection() {
+ try {
+ await loadConfiguration();
+ } catch (error) {
+ console.error('Failed to load configuration:', error);
+ showNotification('Ошибка загрузки конфигурации', 'error');
+ }
+}
+
+// Load current configuration
+async function loadConfiguration() {
+ try {
+ const response = await fetch('/api/config/get', {
+ method: 'GET',
+ credentials: 'same-origin'
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+ }
+
+ const data = await response.json();
+ currentConfig = data.config_data;
+ configMeta = {
+ path: data.config_path,
+ format: data.config_format,
+ isWritable: data.is_writable
+ };
+
+ renderConfigEditor();
+ updateConfigStatus();
+ } catch (error) {
+ console.error('Error loading configuration:', error);
+ throw error;
+ }
+}
+
+// Render configuration editor
+function renderConfigEditor() {
+ const configSection = document.getElementById('config-section');
+
+ const configEditor = `
+
+
+
+
+
+ ${renderConfigGroups()}
+
+
+
+ `;
+
+ configSection.innerHTML = configEditor;
+ updateTexts();
+}
+
+// Render configuration groups
+function renderConfigGroups() {
+ const groups = [
+ {
+ key: 'network',
+ title: 'Сетевые настройки',
+ fields: ['Peers', 'InterfacePeers', 'Listen', 'AllowedPublicKeys']
+ },
+ {
+ key: 'identity',
+ title: 'Идентификация',
+ fields: ['PrivateKey', 'PrivateKeyPath']
+ },
+ {
+ key: 'interface',
+ title: 'Сетевой интерфейс',
+ fields: ['IfName', 'IfMTU']
+ },
+ {
+ key: 'multicast',
+ title: 'Multicast',
+ fields: ['MulticastInterfaces']
+ },
+ {
+ key: 'admin',
+ title: 'Администрирование',
+ fields: ['AdminListen']
+ },
+ {
+ key: 'webui',
+ title: 'Веб-интерфейс',
+ fields: ['WebUI']
+ },
+ {
+ key: 'nodeinfo',
+ title: 'Информация об узле',
+ fields: ['NodeInfo', 'NodeInfoPrivacy', 'LogLookups']
+ }
+ ];
+
+ return groups.map(group => `
+
+
+
+ ${group.fields.map(field => renderConfigField(field)).join('')}
+
+
+ `).join('');
+}
+
+// Render individual config field
+function renderConfigField(fieldName) {
+ const value = currentConfig[fieldName];
+ const fieldType = getFieldType(fieldName, value);
+ const fieldDescription = getFieldDescription(fieldName);
+
+ return `
+
+
+
${fieldDescription}
+
+ ${renderConfigInput(fieldName, value, fieldType)}
+
+
+ `;
+}
+
+// Render config input based on type
+function renderConfigInput(fieldName, value, fieldType) {
+ switch (fieldType) {
+ case 'boolean':
+ return `
+
+ `;
+
+ case 'number':
+ return `
+
+ `;
+
+ case 'string':
+ return `
+
+ `;
+
+ case 'array':
+ return `
+
+
+ Одно значение на строку
+
+ `;
+
+ case 'object':
+ return `
+
+
+ JSON формат
+
+ `;
+
+ case 'private_key':
+ return `
+
+
+ Приватный ключ (только для чтения)
+
+ `;
+
+ default:
+ return `
+
+ `;
+ }
+}
+
+// Get field type for rendering
+function getFieldType(fieldName, value) {
+ if (fieldName === 'PrivateKey') return 'private_key';
+ if (fieldName.includes('MTU') || fieldName === 'Port') return 'number';
+ if (typeof value === 'boolean') return 'boolean';
+ if (typeof value === 'number') return 'number';
+ if (typeof value === 'string') return 'string';
+ if (Array.isArray(value)) return 'array';
+ if (typeof value === 'object') return 'object';
+ return 'string';
+}
+
+// Get field description
+function getFieldDescription(fieldName) {
+ const descriptions = {
+ 'Peers': 'Список исходящих peer соединений (например: tls://адрес:порт)',
+ 'InterfacePeers': 'Peer соединения по интерфейсам',
+ 'Listen': 'Адреса для входящих соединений',
+ 'AllowedPublicKeys': 'Разрешенные публичные ключи для входящих соединений',
+ 'PrivateKey': 'Приватный ключ узла (НЕ ПЕРЕДАВАЙТЕ НИКОМУ!)',
+ 'PrivateKeyPath': 'Путь к файлу с приватным ключом в формате PEM',
+ 'IfName': 'Имя TUN интерфейса ("auto" для автовыбора, "none" для отключения)',
+ 'IfMTU': 'Максимальный размер передаваемого блока (MTU) для TUN интерфейса',
+ 'MulticastInterfaces': 'Настройки multicast интерфейсов для обнаружения peers',
+ 'AdminListen': 'Адрес для подключения админского интерфейса',
+ 'WebUI': 'Настройки веб-интерфейса',
+ 'NodeInfo': 'Дополнительная информация об узле (видна всей сети)',
+ 'NodeInfoPrivacy': 'Скрыть информацию о платформе и версии',
+ 'LogLookups': 'Логировать поиск peers и узлов'
+ };
+
+ return descriptions[fieldName] || 'Параметр конфигурации';
+}
+
+// Update config value
+function updateConfigValue(fieldName, value) {
+ if (currentConfig) {
+ currentConfig[fieldName] = value;
+ markConfigAsModified();
+ }
+}
+
+// Update array config value
+function updateConfigArrayValue(fieldName, value) {
+ if (currentConfig) {
+ const lines = value.split('\n').filter(line => line.trim() !== '');
+ currentConfig[fieldName] = lines;
+ markConfigAsModified();
+ }
+}
+
+// Update object config value
+function updateConfigObjectValue(fieldName, value) {
+ if (currentConfig) {
+ try {
+ currentConfig[fieldName] = JSON.parse(value);
+ markConfigAsModified();
+ } catch (error) {
+ console.error('Invalid JSON for field', fieldName, ':', error);
+ }
+ }
+}
+
+// Mark config as modified
+function markConfigAsModified() {
+ const saveButton = document.querySelector('.save-btn');
+ if (saveButton) {
+ saveButton.classList.add('modified');
+ }
+}
+
+// Toggle config group
+function toggleConfigGroup(groupKey) {
+ const content = document.getElementById(`config-group-${groupKey}`);
+ const icon = content.parentNode.querySelector('.toggle-icon');
+
+ if (content.style.display === 'none') {
+ content.style.display = 'block';
+ icon.textContent = '▼';
+ } else {
+ content.style.display = 'none';
+ icon.textContent = '▶';
+ }
+}
+
+// Update config status display
+function updateConfigStatus() {
+ // This function could show config validation status, etc.
+}
+
+// Refresh configuration
+async function refreshConfiguration() {
+ try {
+ await loadConfiguration();
+ showNotification('Конфигурация обновлена', 'success');
+ } catch (error) {
+ showNotification('Ошибка обновления конфигурации', 'error');
+ }
+}
+
+// Save configuration
+async function saveConfiguration() {
+ if (!configMeta.isWritable) {
+ showNotification('Файл конфигурации доступен только для чтения', 'error');
+ return;
+ }
+
+ showModal({
+ title: 'config_save_confirm_title',
+ content: `
+
+
Вы уверены, что хотите сохранить изменения в конфигурационный файл?
+
+
Файл: ${configMeta.path}
+
Формат: ${configMeta.format.toUpperCase()}
+
Резервная копия: Будет создана автоматически
+
+
+ ⚠️ Внимание: Неправильная конфигурация может привести к сбою работы узла!
+
+
+ `,
+ buttons: [
+ {
+ text: 'modal_cancel',
+ type: 'secondary',
+ action: 'close'
+ },
+ {
+ text: 'save_config',
+ type: 'danger',
+ callback: () => {
+ confirmSaveConfiguration();
+ return true; // Close modal
+ }
+ }
+ ]
+ });
+}
+
+// Confirm and perform save
+async function confirmSaveConfiguration() {
+
+ try {
+ const response = await fetch('/api/config/set', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ credentials: 'same-origin',
+ body: JSON.stringify({
+ config_data: currentConfig,
+ config_path: configMeta.path,
+ format: configMeta.format
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+ }
+
+ const data = await response.json();
+
+ if (data.success) {
+ showNotification(`Конфигурация сохранена: ${data.config_path}`, 'success');
+ if (data.backup_path) {
+ showNotification(`Резервная копия: ${data.backup_path}`, 'info');
+ }
+
+ // Remove modified indicator
+ const saveButton = document.querySelector('.save-btn');
+ if (saveButton) {
+ saveButton.classList.remove('modified');
+ }
+ } else {
+ showNotification(`Ошибка сохранения: ${data.message}`, 'error');
+ }
+ } catch (error) {
+ console.error('Error saving configuration:', error);
+ showNotification('Ошибка сохранения конфигурации', 'error');
+ }
+}
\ No newline at end of file
diff --git a/src/webui/static/index.html b/src/webui/static/index.html
index df76aee0..d0d1e0a9 100644
--- a/src/webui/static/index.html
+++ b/src/webui/static/index.html
@@ -10,16 +10,12 @@
+
-
-
-
-
-
-
Основные настройки
-
Базовая конфигурация узла
-
Функция в разработке...
-
-
-
-
Сетевые настройки
-
Параметры сетевого взаимодействия
-
Функция в разработке...
-
-
+
Загрузка конфигурации...
diff --git a/src/webui/static/lang/en.js b/src/webui/static/lang/en.js
index 0d0d91ae..c48eb20a 100644
--- a/src/webui/static/lang/en.js
+++ b/src/webui/static/lang/en.js
@@ -98,5 +98,18 @@ window.translations.en = {
'add_peer_modal_description': 'Enter peer URI to connect to a new network node',
'peer_uri_label': 'Peer URI',
'peer_uri_placeholder': 'tcp://example.com:54321',
- 'peer_uri_help': 'Examples: tcp://example.com:54321, tls://peer.yggdrasil.network:443'
+ 'peer_uri_help': 'Examples: tcp://example.com:54321, tls://peer.yggdrasil.network:443',
+
+ // Configuration
+ 'configuration_file': 'Configuration File',
+ 'refresh': 'Refresh',
+ 'save_config': 'Save',
+ 'config_save_success': 'Configuration saved successfully',
+ 'config_save_error': 'Error saving configuration',
+ 'config_load_error': 'Error loading configuration',
+ 'config_readonly': 'Configuration file is read-only',
+ 'config_save_confirm_title': 'Confirm Save',
+ 'config_save_confirm_text': 'Are you sure you want to save changes to the configuration file?',
+ 'config_backup_info': 'Backup will be created automatically',
+ 'config_warning': '⚠️ Warning: Incorrect configuration may cause node failure!'
};
\ No newline at end of file
diff --git a/src/webui/static/lang/ru.js b/src/webui/static/lang/ru.js
index 5ff168ca..43594121 100644
--- a/src/webui/static/lang/ru.js
+++ b/src/webui/static/lang/ru.js
@@ -98,5 +98,18 @@ window.translations.ru = {
'add_peer_modal_description': 'Введите URI пира для подключения к новому узлу сети',
'peer_uri_label': 'URI пира',
'peer_uri_placeholder': 'tcp://example.com:54321',
- 'peer_uri_help': 'Примеры: tcp://example.com:54321, tls://peer.yggdrasil.network:443'
+ 'peer_uri_help': 'Примеры: tcp://example.com:54321, tls://peer.yggdrasil.network:443',
+
+ // Configuration
+ 'configuration_file': 'Файл конфигурации',
+ 'refresh': 'Обновить',
+ 'save_config': 'Сохранить',
+ 'config_save_success': 'Конфигурация сохранена успешно',
+ 'config_save_error': 'Ошибка сохранения конфигурации',
+ 'config_load_error': 'Ошибка загрузки конфигурации',
+ 'config_readonly': 'Файл конфигурации доступен только для чтения',
+ 'config_save_confirm_title': 'Подтверждение сохранения',
+ 'config_save_confirm_text': 'Вы уверены, что хотите сохранить изменения в конфигурационный файл?',
+ 'config_backup_info': 'Резервная копия будет создана автоматически',
+ 'config_warning': '⚠️ Внимание: Неправильная конфигурация может привести к сбою работы узла!'
};
\ No newline at end of file
diff --git a/src/webui/static/main.js b/src/webui/static/main.js
index 1a048899..42ec4cf2 100644
--- a/src/webui/static/main.js
+++ b/src/webui/static/main.js
@@ -167,6 +167,11 @@ function showSection(sectionName) {
if (event && event.target) {
event.target.closest('.nav-item').classList.add('active');
}
+
+ // Initialize section-specific functionality
+ if (sectionName === 'config') {
+ initConfigSection();
+ }
}
/**
diff --git a/src/webui/static/style.css b/src/webui/static/style.css
index 53c7137c..12dbcea4 100644
--- a/src/webui/static/style.css
+++ b/src/webui/static/style.css
@@ -114,16 +114,6 @@
--shadow-heavy: rgba(0, 0, 0, 0.5);
}
-/* Dark theme progress bar adjustments */
-[data-theme="dark"] .progress-timer-container {
- background: rgba(0, 0, 0, 0.4);
-}
-
-[data-theme="dark"] .progress-timer-bar {
- background: linear-gradient(90deg, #3498db, #27ae60);
- box-shadow: 0 0 10px rgba(52, 152, 219, 0.3);
-}
-
* {
margin: 0;
padding: 0;
@@ -138,24 +128,6 @@ body {
overflow: hidden; /* Prevent body scroll */
}
-/* Decorative progress bar */
-.progress-timer-container {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- height: 4px;
- background: rgba(255, 255, 255, 0.2);
- z-index: 1000;
-}
-
-.progress-timer-bar {
- height: 100%;
- background: linear-gradient(90deg, #3498db, #2ecc71);
- width: 100%;
- box-shadow: 0 0 10px rgba(52, 152, 219, 0.5);
-}
-
.container {
display: grid;
grid-template-columns: 250px 1fr;
@@ -1563,4 +1535,380 @@ button[onclick="copyNodeKey()"]:hover {
.modal-btn {
width: 100%;
}
+}
+
+/* Configuration Editor Styles */
+.config-container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 20px;
+}
+
+.config-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 30px;
+ padding: 20px;
+ background: var(--bg-info-card);
+ border: 1px solid var(--border-card);
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.config-info h3 {
+ margin: 0 0 10px 0;
+ color: var(--text-heading);
+ font-size: 1.5em;
+}
+
+.config-meta {
+ display: flex;
+ gap: 15px;
+ flex-wrap: wrap;
+ align-items: center;
+}
+
+.config-path {
+ font-family: 'Courier New', monospace;
+ background: var(--bg-nav-item);
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 0.9em;
+ color: var(--text-body);
+ max-width: 300px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.config-format {
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 0.8em;
+ font-weight: bold;
+ text-transform: uppercase;
+}
+
+.config-format.json {
+ background: #e3f2fd;
+ color: #1976d2;
+}
+
+.config-format.hjson {
+ background: #f3e5f5;
+ color: #7b1fa2;
+}
+
+.config-status {
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 0.8em;
+ font-weight: bold;
+}
+
+.config-status.writable {
+ background: var(--bg-success);
+ color: var(--text-success);
+}
+
+.config-status.readonly {
+ background: var(--bg-warning);
+ color: var(--text-warning);
+}
+
+.config-actions {
+ display: flex;
+ gap: 10px;
+}
+
+.refresh-btn {
+ background: var(--bg-nav-active);
+ color: white;
+}
+
+.save-btn {
+ background: var(--bg-success-dark);
+ color: white;
+}
+
+.save-btn.modified {
+ background: var(--bg-warning-dark);
+ animation: pulse 2s infinite;
+}
+
+@keyframes pulse {
+ 0% { opacity: 1; }
+ 50% { opacity: 0.7; }
+ 100% { opacity: 1; }
+}
+
+/* Configuration Groups */
+.config-groups {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.config-group {
+ background: var(--bg-info-card);
+ border: 1px solid var(--border-card);
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.config-group-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 15px 20px;
+ background: var(--bg-nav-item);
+ cursor: pointer;
+ transition: background-color 0.2s;
+}
+
+.config-group-header:hover {
+ background: var(--bg-nav-hover);
+}
+
+.config-group-header h4 {
+ margin: 0;
+ color: var(--text-heading);
+ font-size: 1.2em;
+}
+
+.toggle-icon {
+ font-size: 1.2em;
+ color: var(--text-muted);
+ transition: transform 0.2s;
+}
+
+.config-group-content {
+ padding: 0;
+}
+
+/* Configuration Fields */
+.config-field {
+ padding: 20px;
+ border-bottom: 1px solid var(--border-card);
+}
+
+.config-field:last-child {
+ border-bottom: none;
+}
+
+.config-field-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 8px;
+}
+
+.config-field-header label {
+ font-weight: bold;
+ color: var(--text-heading);
+ font-family: 'Courier New', monospace;
+}
+
+.field-type {
+ padding: 2px 6px;
+ background: var(--bg-nav-item);
+ color: var(--text-muted);
+ border-radius: 3px;
+ font-size: 0.8em;
+ font-weight: normal;
+}
+
+.config-field-description {
+ color: var(--text-muted);
+ font-size: 0.9em;
+ margin-bottom: 12px;
+ line-height: 1.4;
+}
+
+.config-field-input {
+ position: relative;
+}
+
+/* Input Styles */
+.config-input, .config-textarea {
+ width: 100%;
+ padding: 10px 12px;
+ border: 1px solid var(--border-card);
+ border-radius: 4px;
+ font-family: 'Courier New', monospace;
+ font-size: 0.9em;
+ background: var(--bg-info-card);
+ color: var(--text-body);
+ transition: border-color 0.2s;
+}
+
+.config-input:focus, .config-textarea:focus {
+ outline: none;
+ border-color: var(--border-hover);
+ box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
+}
+
+.config-textarea {
+ resize: vertical;
+ min-height: 80px;
+ font-family: 'Courier New', monospace;
+}
+
+.private-key {
+ font-family: monospace;
+ letter-spacing: 1px;
+}
+
+/* Switch Styles */
+.switch {
+ position: relative;
+ display: inline-block;
+ width: 60px;
+ height: 34px;
+}
+
+.switch input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+
+.slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #ccc;
+ transition: 0.4s;
+ border-radius: 34px;
+}
+
+.slider:before {
+ position: absolute;
+ content: "";
+ height: 26px;
+ width: 26px;
+ left: 4px;
+ bottom: 4px;
+ background-color: white;
+ transition: 0.4s;
+ border-radius: 50%;
+}
+
+input:checked + .slider {
+ background-color: var(--bg-nav-active);
+}
+
+input:focus + .slider {
+ box-shadow: 0 0 1px var(--bg-nav-active);
+}
+
+input:checked + .slider:before {
+ transform: translateX(26px);
+}
+
+/* Array and Object Input Styles */
+.array-input, .object-input, .private-key-input {
+ position: relative;
+}
+
+.array-input small, .object-input small, .private-key-input small {
+ display: block;
+ margin-top: 5px;
+ color: var(--text-muted);
+ font-size: 0.8em;
+}
+
+/* Save Confirmation Modal */
+.save-config-confirmation {
+ text-align: center;
+}
+
+.config-save-info {
+ background: var(--bg-nav-item);
+ padding: 15px;
+ border-radius: 4px;
+ margin: 15px 0;
+ text-align: left;
+}
+
+.config-save-info p {
+ margin: 5px 0;
+ font-family: 'Courier New', monospace;
+ font-size: 0.9em;
+}
+
+.warning {
+ background: var(--bg-warning);
+ border: 1px solid var(--border-warning);
+ color: var(--text-warning);
+ padding: 15px;
+ border-radius: 4px;
+ margin-top: 15px;
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ .config-container {
+ padding: 10px;
+ }
+
+ .config-header {
+ flex-direction: column;
+ gap: 15px;
+ align-items: stretch;
+ }
+
+ .config-meta {
+ flex-direction: column;
+ gap: 10px;
+ align-items: flex-start;
+ }
+
+ .config-path {
+ max-width: 100%;
+ }
+
+ .config-actions {
+ justify-content: center;
+ }
+
+ .config-field {
+ padding: 15px;
+ }
+
+ .config-field-header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 5px;
+ }
+}
+
+/* Dark Theme Support */
+[data-theme="dark"] .config-container,
+[data-theme="dark"] .config-group,
+[data-theme="dark"] .config-header {
+ background: var(--bg-info-card);
+ border-color: var(--border-card);
+}
+
+[data-theme="dark"] .config-input,
+[data-theme="dark"] .config-textarea {
+ background: var(--bg-nav-item);
+ border-color: var(--border-card);
+ color: var(--text-body);
+}
+
+[data-theme="dark"] .config-save-info {
+ background: var(--bg-nav-item);
+}
+
+[data-theme="dark"] .warning {
+ background: rgba(255, 193, 7, 0.1);
+ border-color: var(--border-warning);
+ color: var(--text-warning);
}
\ No newline at end of file