From 0d0f524071cc06885c517cd298a78f19a4799700 Mon Sep 17 00:00:00 2001 From: Andy Oknen Date: Fri, 15 Aug 2025 15:59:00 +0000 Subject: [PATCH] Implement JSON configuration editor in WebUI with save and restart functionality. Enhance configuration handling by converting data to JSON format and adding validation features. Update styles for improved user experience. --- src/webui/server.go | 85 +++++-- src/webui/static/config-editor.js | 313 +++++++++++++++++++++++++ src/webui/static/config.js | 375 ++++-------------------------- src/webui/static/index.html | 1 + src/webui/static/lang/en.js | 5 + src/webui/static/lang/ru.js | 5 + src/webui/static/style.css | 218 ++++++++++++++++- 7 files changed, 650 insertions(+), 352 deletions(-) create mode 100644 src/webui/static/config-editor.js diff --git a/src/webui/server.go b/src/webui/server.go index 0848d101..73f24979 100644 --- a/src/webui/server.go +++ b/src/webui/server.go @@ -8,8 +8,10 @@ import ( "fmt" "net" "net/http" + "os" "strings" "sync" + "syscall" "time" "github.com/yggdrasil-network/yggdrasil-go/src/admin" @@ -384,23 +386,25 @@ func (w *WebUIServer) callAdminHandler(command string, args map[string]interface // 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"` + ConfigPath string `json:"config_path"` + ConfigFormat string `json:"config_format"` + ConfigJSON string `json:"config_json"` + IsWritable bool `json:"is_writable"` } type ConfigSetRequest struct { - ConfigData interface{} `json:"config_data"` - ConfigPath string `json:"config_path,omitempty"` - Format string `json:"format,omitempty"` + ConfigJSON string `json:"config_json"` + ConfigPath string `json:"config_path,omitempty"` + Format string `json:"format,omitempty"` + Restart bool `json:"restart,omitempty"` } type ConfigSetResponse struct { - Success bool `json:"success"` - Message string `json:"message"` - ConfigPath string `json:"config_path"` - BackupPath string `json:"backup_path,omitempty"` + Success bool `json:"success"` + Message string `json:"message"` + ConfigPath string `json:"config_path"` + BackupPath string `json:"backup_path,omitempty"` + RestartRequired bool `json:"restart_required"` } // getConfigHandler handles configuration file reading @@ -418,10 +422,18 @@ func (w *WebUIServer) getConfigHandler(rw http.ResponseWriter, r *http.Request) return } + // Convert config data to formatted JSON string + configBytes, err := json.MarshalIndent(configInfo.Data, "", " ") + if err != nil { + w.log.Errorf("Failed to marshal config to JSON: %v", err) + http.Error(rw, "Failed to format configuration", http.StatusInternalServerError) + return + } + response := ConfigResponse{ ConfigPath: configInfo.Path, ConfigFormat: configInfo.Format, - ConfigData: configInfo.Data, + ConfigJSON: string(configBytes), IsWritable: configInfo.Writable, } @@ -445,8 +457,19 @@ func (w *WebUIServer) setConfigHandler(rw http.ResponseWriter, r *http.Request) return } + // Parse JSON configuration + var configData interface{} + if err := json.Unmarshal([]byte(req.ConfigJSON), &configData); err != nil { + response := ConfigSetResponse{ + Success: false, + Message: fmt.Sprintf("Invalid JSON configuration: %v", err), + } + w.writeJSONResponse(rw, response) + return + } + // Use config package to save configuration - err := config.SaveConfig(req.ConfigData, req.ConfigPath, req.Format) + err := config.SaveConfig(configData, req.ConfigPath, req.Format) if err != nil { response := ConfigSetResponse{ Success: false, @@ -464,11 +487,19 @@ func (w *WebUIServer) setConfigHandler(rw http.ResponseWriter, r *http.Request) } response := ConfigSetResponse{ - Success: true, - Message: "Configuration saved successfully", - ConfigPath: configPath, - BackupPath: configPath + ".backup", + Success: true, + Message: "Configuration saved successfully", + ConfigPath: configPath, + BackupPath: configPath + ".backup", + RestartRequired: req.Restart, } + + // If restart is requested, trigger server restart + if req.Restart { + w.log.Infof("Configuration saved with restart request") + go w.restartServer() + } + w.writeJSONResponse(rw, response) } @@ -556,3 +587,23 @@ func (w *WebUIServer) Stop() error { } return nil } + +// restartServer triggers a graceful restart of the Yggdrasil process +func (w *WebUIServer) restartServer() { + w.log.Infof("Initiating server restart after configuration change") + + // Give some time for the response to be sent + time.Sleep(1 * time.Second) + + // Send SIGUSR1 signal to trigger a graceful restart + // This assumes the main process handles SIGUSR1 for restart + proc, err := os.FindProcess(os.Getpid()) + if err != nil { + w.log.Errorf("Failed to find current process: %v", err) + return + } + + if err := proc.Signal(syscall.SIGUSR1); err != nil { + w.log.Errorf("Failed to send restart signal: %v", err) + } +} diff --git a/src/webui/static/config-editor.js b/src/webui/static/config-editor.js new file mode 100644 index 00000000..d0f69468 --- /dev/null +++ b/src/webui/static/config-editor.js @@ -0,0 +1,313 @@ +// JSON Editor functionality for configuration + +// Initialize JSON editor with basic syntax highlighting and features +function initJSONEditor() { + const textarea = document.getElementById('config-json-textarea'); + const lineNumbersContainer = document.getElementById('line-numbers-container'); + const statusElement = document.getElementById('editor-status'); + const cursorElement = document.getElementById('cursor-position'); + + if (!textarea) return; + + // Set initial content + textarea.value = currentConfigJSON || ''; + + // Update line numbers + updateLineNumbers(); + + // Add event listeners + textarea.addEventListener('input', function() { + updateLineNumbers(); + updateEditorStatus(); + onConfigChange(); + }); + + textarea.addEventListener('scroll', syncLineNumbers); + textarea.addEventListener('keydown', handleEditorKeydown); + textarea.addEventListener('selectionchange', updateCursorPosition); + textarea.addEventListener('click', updateCursorPosition); + textarea.addEventListener('keyup', updateCursorPosition); + + // Initial status update + updateEditorStatus(); + updateCursorPosition(); +} + +// Update line numbers +function updateLineNumbers() { + const textarea = document.getElementById('config-json-textarea'); + const lineNumbersContainer = document.getElementById('line-numbers-container'); + + if (!textarea || !lineNumbersContainer) return; + + const lines = textarea.value.split('\n'); + const lineNumbers = lines.map((_, index) => + `${index + 1}` + ).join(''); + + lineNumbersContainer.innerHTML = lineNumbers; +} + +// Sync line numbers scroll with textarea +function syncLineNumbers() { + const textarea = document.getElementById('config-json-textarea'); + const lineNumbersContainer = document.getElementById('line-numbers-container'); + + if (!textarea || !lineNumbersContainer) return; + + lineNumbersContainer.scrollTop = textarea.scrollTop; +} + +// Toggle line numbers visibility +function toggleLineNumbers() { + const checkbox = document.getElementById('line-numbers'); + const lineNumbersContainer = document.getElementById('line-numbers-container'); + const editorWrapper = document.querySelector('.editor-wrapper'); + + if (checkbox.checked) { + lineNumbersContainer.style.display = 'block'; + editorWrapper.classList.add('with-line-numbers'); + } else { + lineNumbersContainer.style.display = 'none'; + editorWrapper.classList.remove('with-line-numbers'); + } +} + +// Handle special editor keydown events +function handleEditorKeydown(event) { + const textarea = event.target; + + // Tab handling for indentation + if (event.key === 'Tab') { + event.preventDefault(); + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + + if (event.shiftKey) { + // Remove indentation + const beforeCursor = textarea.value.substring(0, start); + const lineStart = beforeCursor.lastIndexOf('\n') + 1; + const currentLine = textarea.value.substring(lineStart, end); + + if (currentLine.startsWith(' ')) { + textarea.value = textarea.value.substring(0, lineStart) + + currentLine.substring(2) + + textarea.value.substring(end); + textarea.selectionStart = Math.max(lineStart, start - 2); + textarea.selectionEnd = end - 2; + } + } else { + // Add indentation + textarea.value = textarea.value.substring(0, start) + + ' ' + + textarea.value.substring(end); + textarea.selectionStart = start + 2; + textarea.selectionEnd = start + 2; + } + + updateLineNumbers(); + updateEditorStatus(); + } + + // Auto-closing brackets and quotes + if (event.key === '{') { + insertMatchingCharacter(textarea, '{', '}'); + event.preventDefault(); + } else if (event.key === '[') { + insertMatchingCharacter(textarea, '[', ']'); + event.preventDefault(); + } else if (event.key === '"') { + insertMatchingCharacter(textarea, '"', '"'); + event.preventDefault(); + } +} + +// Insert matching character (auto-closing) +function insertMatchingCharacter(textarea, open, close) { + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const selectedText = textarea.value.substring(start, end); + + textarea.value = textarea.value.substring(0, start) + + open + selectedText + close + + textarea.value.substring(end); + + if (selectedText) { + textarea.selectionStart = start + 1; + textarea.selectionEnd = start + 1 + selectedText.length; + } else { + textarea.selectionStart = start + 1; + textarea.selectionEnd = start + 1; + } +} + +// Update editor status (validation) +function updateEditorStatus() { + const textarea = document.getElementById('config-json-textarea'); + const statusElement = document.getElementById('editor-status'); + + if (!textarea || !statusElement) return; + + try { + if (textarea.value.trim() === '') { + statusElement.textContent = 'Пустая конфигурация'; + statusElement.className = 'status-text warning'; + return; + } + + JSON.parse(textarea.value); + statusElement.textContent = 'Валидный JSON'; + statusElement.className = 'status-text success'; + } catch (error) { + statusElement.textContent = `Ошибка JSON: ${error.message}`; + statusElement.className = 'status-text error'; + } +} + +// Update cursor position display +function updateCursorPosition() { + const textarea = document.getElementById('config-json-textarea'); + const cursorElement = document.getElementById('cursor-position'); + + if (!textarea || !cursorElement) return; + + const cursorPos = textarea.selectionStart; + const beforeCursor = textarea.value.substring(0, cursorPos); + const line = beforeCursor.split('\n').length; + const column = beforeCursor.length - beforeCursor.lastIndexOf('\n'); + + cursorElement.textContent = `Строка ${line}, Столбец ${column}`; +} + +// Format JSON with proper indentation +function formatJSON() { + const textarea = document.getElementById('config-json-textarea'); + + if (!textarea) return; + + try { + const parsed = JSON.parse(textarea.value); + const formatted = JSON.stringify(parsed, null, 2); + textarea.value = formatted; + updateLineNumbers(); + updateEditorStatus(); + showNotification('JSON отформатирован', 'success'); + } catch (error) { + showNotification(`Ошибка форматирования: ${error.message}`, 'error'); + } +} + +// Validate JSON configuration +function validateJSON() { + const textarea = document.getElementById('config-json-textarea'); + + if (!textarea) return; + + try { + JSON.parse(textarea.value); + showNotification('JSON конфигурация валидна', 'success'); + } catch (error) { + showNotification(`Ошибка валидации JSON: ${error.message}`, 'error'); + } +} + +// Save configuration +async function saveConfiguration(restart = false) { + const textarea = document.getElementById('config-json-textarea'); + + if (!textarea) { + showNotification('Редактор не найден', 'error'); + return; + } + + // Validate JSON before saving + try { + JSON.parse(textarea.value); + } catch (error) { + showNotification(`Невозможно сохранить: ${error.message}`, 'error'); + return; + } + + try { + const response = await fetch('/api/config/set', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'same-origin', + body: JSON.stringify({ + config_json: textarea.value, + restart: restart + }) + }); + + const result = await response.json(); + + if (result.success) { + currentConfigJSON = textarea.value; + if (restart) { + showNotification('Конфигурация сохранена. Сервер перезапускается...', 'success'); + } else { + showNotification('Конфигурация сохранена успешно', 'success'); + } + onConfigChange(); // Update button states + } else { + showNotification(`Ошибка сохранения: ${result.message}`, 'error'); + } + } catch (error) { + console.error('Error saving configuration:', error); + showNotification(`Ошибка сохранения: ${error.message}`, 'error'); + } +} + +// Save configuration and restart server +async function saveAndRestartConfiguration() { + const confirmation = confirm('Сохранить конфигурацию и перезапустить сервер?\n\nВнимание: Соединение будет прервано на время перезапуска.'); + + if (confirmation) { + await saveConfiguration(true); + } +} + +// Refresh configuration from server +async function refreshConfiguration() { + const textarea = document.getElementById('config-json-textarea'); + + // Check if there are unsaved changes + if (textarea && textarea.value !== currentConfigJSON) { + const confirmation = confirm('У вас есть несохраненные изменения. Продолжить обновление?'); + if (!confirmation) { + return; + } + } + + try { + await loadConfiguration(); + showNotification('Конфигурация обновлена', 'success'); + } catch (error) { + showNotification('Ошибка обновления конфигурации', 'error'); + } +} + +// Update configuration status +function updateConfigStatus() { + // This function can be used to show additional status information + // Currently handled by updateEditorStatus +} + +// Handle configuration changes +function onConfigChange() { + // Mark configuration as modified + const saveBtn = document.querySelector('.save-btn'); + const restartBtn = document.querySelector('.restart-btn'); + + const textarea = document.getElementById('config-json-textarea'); + const hasChanges = textarea && textarea.value !== currentConfigJSON; + + if (saveBtn) { + saveBtn.style.fontWeight = hasChanges ? 'bold' : 'normal'; + } + if (restartBtn) { + restartBtn.style.fontWeight = hasChanges ? 'bold' : 'normal'; + } +} \ No newline at end of file diff --git a/src/webui/static/config.js b/src/webui/static/config.js index 28e8d0f5..575c200c 100644 --- a/src/webui/static/config.js +++ b/src/webui/static/config.js @@ -1,7 +1,8 @@ // Configuration management functions -let currentConfig = null; +let currentConfigJSON = null; let configMeta = null; +let configEditor = null; // Initialize config section async function initConfigSection() { @@ -26,7 +27,7 @@ async function loadConfiguration() { } const data = await response.json(); - currentConfig = data.config_data; + currentConfigJSON = data.config_json; configMeta = { path: data.config_path, format: data.config_format, @@ -41,7 +42,7 @@ async function loadConfiguration() { } } -// Render configuration editor +// Render configuration editor with JSON syntax highlighting function renderConfigEditor() { const configSection = document.getElementById('config-section'); @@ -62,17 +63,50 @@ function renderConfigEditor() { + + ${configMeta.isWritable ? ` + ` : ''}
-
- ${renderConfigGroups()} +
+
+ JSON Конфигурация +
+ + + + +
+
+
+
+ +
+
+ + +
@@ -80,332 +114,5 @@ function renderConfigEditor() { 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 + initJSONEditor(); +} \ No newline at end of file diff --git a/src/webui/static/index.html b/src/webui/static/index.html index d0d1e0a9..99f7e15b 100644 --- a/src/webui/static/index.html +++ b/src/webui/static/index.html @@ -11,6 +11,7 @@ + diff --git a/src/webui/static/lang/en.js b/src/webui/static/lang/en.js index c48eb20a..6b7f2a31 100644 --- a/src/webui/static/lang/en.js +++ b/src/webui/static/lang/en.js @@ -104,6 +104,11 @@ window.translations.en = { 'configuration_file': 'Configuration File', 'refresh': 'Refresh', 'save_config': 'Save', + 'save_and_restart': 'Save and Restart', + 'format': 'Format', + 'validate': 'Validate', + 'json_configuration': 'JSON Configuration', + 'line_numbers': 'Line Numbers', 'config_save_success': 'Configuration saved successfully', 'config_save_error': 'Error saving configuration', 'config_load_error': 'Error loading configuration', diff --git a/src/webui/static/lang/ru.js b/src/webui/static/lang/ru.js index 43594121..b4b0d3c5 100644 --- a/src/webui/static/lang/ru.js +++ b/src/webui/static/lang/ru.js @@ -104,6 +104,11 @@ window.translations.ru = { 'configuration_file': 'Файл конфигурации', 'refresh': 'Обновить', 'save_config': 'Сохранить', + 'save_and_restart': 'Сохранить и перезапустить', + 'format': 'Форматировать', + 'validate': 'Проверить', + 'json_configuration': 'JSON Конфигурация', + 'line_numbers': 'Номера строк', 'config_save_success': 'Конфигурация сохранена успешно', 'config_save_error': 'Ошибка сохранения конфигурации', 'config_load_error': 'Ошибка загрузки конфигурации', diff --git a/src/webui/static/style.css b/src/webui/static/style.css index 12dbcea4..81630570 100644 --- a/src/webui/static/style.css +++ b/src/webui/static/style.css @@ -1911,4 +1911,220 @@ input:checked + .slider:before { background: rgba(255, 193, 7, 0.1); border-color: var(--border-warning); color: var(--text-warning); -} \ No newline at end of file +} + +/* JSON Editor Styles */ +.config-json-editor { + 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); +} + +.editor-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 20px; + background: var(--bg-nav-item); + border-bottom: 1px solid var(--border-card); +} + +.editor-title { + font-weight: bold; + color: var(--text-heading); + font-size: 1.1em; +} + +.editor-controls { + display: flex; + align-items: center; + gap: 15px; +} + +.line-numbers-toggle { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.9em; + color: var(--text-muted); +} + +.line-numbers-toggle input[type="checkbox"] { + margin: 0; +} + +.editor-wrapper { + position: relative; + display: flex; + height: 500px; + font-family: 'Courier New', Monaco, 'Lucida Console', monospace; +} + +.editor-wrapper.with-line-numbers .json-editor { + padding-left: 60px; +} + +.line-numbers { + position: absolute; + left: 0; + top: 0; + width: 50px; + height: 100%; + background: var(--bg-nav-item); + border-right: 1px solid var(--border-card); + overflow: hidden; + font-size: 0.9em; + line-height: 1.4; + padding: 10px 5px; + box-sizing: border-box; + z-index: 1; +} + +.line-number { + display: block; + text-align: right; + color: var(--text-muted); + user-select: none; + padding-right: 8px; + min-height: 1.4em; +} + +.json-editor { + flex: 1; + border: none; + outline: none; + resize: none; + font-family: 'Courier New', Monaco, 'Lucida Console', monospace; + font-size: 14px; + line-height: 1.4; + padding: 10px; + background: var(--bg-info-card); + color: var(--text-body); + white-space: pre; + overflow-wrap: normal; + overflow-x: auto; + tab-size: 2; +} + +.json-editor:focus { + outline: none; + background: var(--bg-info-card); +} + +.json-editor::placeholder { + color: var(--text-muted); + font-style: italic; +} + +.editor-status { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 15px; + background: var(--bg-nav-item); + border-top: 1px solid var(--border-card); + font-size: 0.9em; +} + +.status-text { + font-weight: 500; +} + +.status-text.success { + color: var(--text-success); +} + +.status-text.error { + color: var(--text-error); +} + +.status-text.warning { + color: var(--text-warning); +} + +.cursor-position { + color: var(--text-muted); + font-family: 'Courier New', monospace; + font-size: 0.8em; +} + +/* Additional action buttons */ +.format-btn { + background: var(--bg-nav-active); + color: white; +} + +.validate-btn { + background: var(--bg-warning-dark); + color: white; +} + +.restart-btn { + background: var(--text-error); + color: white; +} + +.restart-btn:hover { + background: #c62828; +} + +/* JSON Syntax highlighting (basic) */ +.json-editor { + color: var(--text-body); +} + +/* Mobile responsiveness for JSON editor */ +@media (max-width: 768px) { + .editor-header { + flex-direction: column; + gap: 10px; + align-items: stretch; + } + + .editor-controls { + justify-content: center; + } + + .editor-wrapper { + height: 400px; + } + + .json-editor { + font-size: 12px; + } + + .line-numbers { + width: 40px; + padding: 10px 3px; + font-size: 0.8em; + } + + .editor-wrapper.with-line-numbers .json-editor { + padding-left: 45px; + } + + .config-actions { + flex-wrap: wrap; + } + + .action-btn { + flex: 1; + min-width: 120px; + } +} + +/* Dark theme support for JSON editor */ +[data-theme="dark"] .config-json-editor, +[data-theme="dark"] .editor-header, +[data-theme="dark"] .editor-status, +[data-theme="dark"] .line-numbers { + background: var(--bg-info-card); + border-color: var(--border-card); +} + +[data-theme="dark"] .json-editor { + background: var(--bg-info-card); + color: var(--text-body); +} \ No newline at end of file