diff --git a/templates/index.html b/templates/index.html index 5c12b443..c2ae23a9 100644 --- a/templates/index.html +++ b/templates/index.html @@ -298,6 +298,84 @@ font-size: 11px; opacity: 0.8; } + + .json-container textarea { + border: 2px solid #ddd; + transition: border-color 0.3s; + } + + .json-container textarea:focus { + border-color: #4CAF50; + outline: none; + } + + .json-container textarea.error { + border-color: #f44336; + } + + .json-container textarea.valid { + border-color: #4CAF50; + } + + .save-button { + background-color: #4CAF50; + color: white; + padding: 8px 16px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: background-color 0.3s; + } + + .save-button:hover { + background-color: #45a049; + } + + .save-button:disabled { + background-color: #cccccc; + cursor: not-allowed; + } + + .json-actions { + margin-top: 15px; + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; + } + + .json-actions button { + flex-shrink: 0; + } + + .json-container { + position: relative; + margin-bottom: 15px; + } + + .json-status { + position: absolute; + top: 10px; + right: 10px; + padding: 4px 8px; + border-radius: 3px; + font-size: 11px; + font-weight: bold; + color: white; + } + + .json-status.valid { + background-color: #4CAF50; + } + + .json-status.error { + background-color: #f44336; + } + + .json-status.warning { + background-color: #ff9800; + } @@ -1561,7 +1639,7 @@

Raw Configuration JSON

-

View and copy the complete configuration files. Use these for backup or manual editing.

+

View, edit, and save the complete configuration files directly. ⚠️ Warning: Be careful when editing raw JSON - invalid syntax will prevent saving. Use the "Validate JSON" button to check your changes before saving.

Main Configuration (config.json)

@@ -1572,13 +1650,17 @@
- + +
VALID
+ + +
@@ -1591,13 +1673,17 @@
- + +
VALID
+ + +
@@ -1668,6 +1754,50 @@ } } + function formatJson(elementId) { + const textarea = document.getElementById(elementId); + const jsonText = textarea.value; + + try { + // Parse and re-stringify with proper formatting + const parsed = JSON.parse(jsonText); + const formatted = JSON.stringify(parsed, null, 4); + textarea.value = formatted; + + // Update visual state + textarea.classList.remove('error'); + textarea.classList.add('valid'); + + showMessage('JSON formatted successfully!', 'success'); + } catch (error) { + showMessage(`Cannot format invalid JSON: ${error.message}`, 'error'); + textarea.classList.remove('valid'); + textarea.classList.add('error'); + } + } + + function previewChanges(configType) { + const textareaId = configType === 'main' ? 'main-config-json' : 'secrets-config-json'; + const textarea = document.getElementById(textareaId); + const jsonText = textarea.value; + + try { + // Validate JSON first + const parsed = JSON.parse(jsonText); + + // Show a simple preview of the structure + const configName = configType === 'main' ? 'Main Configuration' : 'Secrets Configuration'; + const previewText = `Preview of ${configName}:\n\n` + + `Top-level keys: ${Object.keys(parsed).join(', ')}\n\n` + + `Total size: ${JSON.stringify(parsed).length} characters\n\n` + + `This will overwrite the current ${configType === 'main' ? 'config.json' : 'config_secrets.json'} file.`; + + alert(previewText); + } catch (error) { + showMessage(`Cannot preview invalid JSON: ${error.message}`, 'error'); + } + } + function validateJson(textareaId, validationId) { const textarea = document.getElementById(textareaId); const validationDiv = document.getElementById(validationId); @@ -1678,6 +1808,10 @@ validationDiv.className = 'json-validation'; validationDiv.style.display = 'block'; + // Update status indicator + const statusId = validationId.replace('-validation', '-status'); + const statusElement = document.getElementById(statusId); + try { // Try to parse the JSON const parsed = JSON.parse(jsonText); @@ -1714,6 +1848,10 @@ // Display results if (warnings.length > 0) { validationDiv.className = 'json-validation warning'; + if (statusElement) { + statusElement.textContent = 'WARNING'; + statusElement.className = 'json-status warning'; + } validationDiv.innerHTML = `
✅ JSON is valid but has warnings:
@@ -1722,6 +1860,10 @@ `; } else { validationDiv.className = 'json-validation success'; + if (statusElement) { + statusElement.textContent = 'VALID'; + statusElement.className = 'json-status valid'; + } validationDiv.innerHTML = `
✅ JSON is valid!
@@ -1735,6 +1877,10 @@ } catch (error) { // JSON parsing failed validationDiv.className = 'json-validation error'; + if (statusElement) { + statusElement.textContent = 'INVALID'; + statusElement.className = 'json-status error'; + } let errorMessage = '❌ Invalid JSON syntax'; let errorDetails = error.message; @@ -1775,6 +1921,90 @@ } } + function saveRawJson(configType) { + const textareaId = configType === 'main' ? 'main-config-json' : 'secrets-config-json'; + const validationId = configType === 'main' ? 'main-config-validation' : 'secrets-config-validation'; + const textarea = document.getElementById(textareaId); + const validationDiv = document.getElementById(validationId); + const jsonText = textarea.value; + + // First validate the JSON + try { + JSON.parse(jsonText); + } catch (error) { + showMessage(`Invalid JSON format: ${error.message}`, 'error'); + validateJson(textareaId, validationId); + return; + } + + // Show confirmation dialog + const configName = configType === 'main' ? 'Main Configuration' : 'Secrets Configuration'; + if (!confirm(`Are you sure you want to save changes to the ${configName}? This will overwrite the current configuration file.`)) { + return; + } + + // Show loading state + const saveButton = event.target; + const originalText = saveButton.textContent; + saveButton.textContent = 'Saving...'; + saveButton.disabled = true; + + // Send to server + fetch("{{ url_for('save_raw_json_route') }}", { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + config_type: configType, + config_data: jsonText + }) + }) + .then(response => response.json()) + .then(data => { + if (data.status === 'success') { + showMessage(data.message, 'success'); + // Update validation to show success + validationDiv.className = 'json-validation success'; + validationDiv.innerHTML = ` +
✅ Configuration saved successfully!
+
+ ✓ JSON is valid
+ ✓ File saved successfully
+ ✓ Configuration updated +
+ `; + validationDiv.style.display = 'block'; + } else { + showMessage(data.message, 'error'); + // Show error in validation area + validationDiv.className = 'json-validation error'; + validationDiv.innerHTML = ` +
❌ Save failed
+
+ Error: ${data.message} +
+ `; + validationDiv.style.display = 'block'; + } + }) + .catch(error => { + showMessage(`Error saving configuration: ${error}`, 'error'); + // Show error in validation area + validationDiv.className = 'json-validation error'; + validationDiv.innerHTML = ` +
❌ Save failed
+
+ Error: ${error} +
+ `; + validationDiv.style.display = 'block'; + }) + .finally(() => { + // Restore button state + saveButton.textContent = originalText; + saveButton.disabled = false; + }); + } + function saveSportsConfig() { // Collect all sports configuration and save const config = { @@ -2541,6 +2771,51 @@ // Set default active tab document.addEventListener("DOMContentLoaded", function() { document.querySelector('.tab-link').click(); + + // Add real-time JSON validation for the raw JSON textareas + const mainConfigTextarea = document.getElementById('main-config-json'); + const secretsConfigTextarea = document.getElementById('secrets-config-json'); + + function addJsonValidationListener(textarea, validationId) { + let validationTimeout; + const statusId = validationId.replace('-validation', '-status'); + const statusElement = document.getElementById(statusId); + + textarea.addEventListener('input', function() { + // Clear previous timeout + clearTimeout(validationTimeout); + + // Set a new timeout to validate after user stops typing + validationTimeout = setTimeout(() => { + const jsonText = textarea.value; + + try { + JSON.parse(jsonText); + textarea.classList.remove('error'); + textarea.classList.add('valid'); + if (statusElement) { + statusElement.textContent = 'VALID'; + statusElement.className = 'json-status valid'; + } + } catch (error) { + textarea.classList.remove('valid'); + textarea.classList.add('error'); + if (statusElement) { + statusElement.textContent = 'INVALID'; + statusElement.className = 'json-status error'; + } + } + }, 500); // Wait 500ms after user stops typing + }); + } + + if (mainConfigTextarea) { + addJsonValidationListener(mainConfigTextarea, 'main-config-validation'); + } + + if (secretsConfigTextarea) { + addJsonValidationListener(secretsConfigTextarea, 'secrets-config-validation'); + } }); diff --git a/web_interface.py b/web_interface.py index 97550c29..760282a8 100644 --- a/web_interface.py +++ b/web_interface.py @@ -286,5 +286,47 @@ def run_action_route(): 'message': f'Error running action: {e}' }), 400 +@app.route('/save_raw_json', methods=['POST']) +def save_raw_json_route(): + try: + data = request.get_json() + config_type = data.get('config_type') + config_data = data.get('config_data') + + if not config_type or not config_data: + return jsonify({ + 'status': 'error', + 'message': 'Missing config_type or config_data' + }), 400 + + if config_type not in ['main', 'secrets']: + return jsonify({ + 'status': 'error', + 'message': 'Invalid config_type. Must be "main" or "secrets"' + }), 400 + + # Validate JSON format + try: + parsed_data = json.loads(config_data) + except json.JSONDecodeError as e: + return jsonify({ + 'status': 'error', + 'message': f'Invalid JSON format: {str(e)}' + }), 400 + + # Save the raw JSON + config_manager.save_raw_file_content(config_type, parsed_data) + + return jsonify({ + 'status': 'success', + 'message': f'{config_type.capitalize()} configuration saved successfully!' + }) + + except Exception as e: + return jsonify({ + 'status': 'error', + 'message': f'Error saving raw JSON: {str(e)}' + }), 400 + if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True) \ No newline at end of file