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 = ` +
+
+
+

Файл конфигурации

+
+ ${configMeta.path} + ${configMeta.format.toUpperCase()} + + ${configMeta.isWritable ? '✏️ Редактируемый' : '🔒 Только чтение'} + +
+
+
+ + ${configMeta.isWritable ? ` + + ` : ''} +
+
+ +
+
+ ${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.title}

+ +
+
+ ${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 ` +
+
+ + ${fieldType} +
+
${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 @@ + - -
-
-
-
@@ -112,19 +108,7 @@
-
-
-

Основные настройки

-

Базовая конфигурация узла

- Функция в разработке... -
- -
-

Сетевые настройки

-

Параметры сетевого взаимодействия

- Функция в разработке... -
-
+
Загрузка конфигурации...
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