mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-11 21:33:00 +00:00
web ui json editor improvements
This commit is contained in:
@@ -298,6 +298,84 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
opacity: 0.8;
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -1561,7 +1639,7 @@
|
|||||||
<div id="raw-json" class="tab-content">
|
<div id="raw-json" class="tab-content">
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h2>Raw Configuration JSON</h2>
|
<h2>Raw Configuration JSON</h2>
|
||||||
<p>View and copy the complete configuration files. Use these for backup or manual editing.</p>
|
<p>View, edit, and save the complete configuration files directly. <strong>⚠️ Warning:</strong> Be careful when editing raw JSON - invalid syntax will prevent saving. Use the "Validate JSON" button to check your changes before saving.</p>
|
||||||
|
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
<h3>Main Configuration (config.json)</h3>
|
<h3>Main Configuration (config.json)</h3>
|
||||||
@@ -1572,13 +1650,17 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Raw JSON:</label>
|
<label>Raw JSON:</label>
|
||||||
<div class="json-container">
|
<div class="json-container">
|
||||||
<textarea id="main-config-json" readonly style="height: 400px; font-family: monospace; font-size: 12px;">{{ main_config_json }}</textarea>
|
<textarea id="main-config-json" style="height: 400px; font-family: monospace; font-size: 12px;">{{ main_config_json }}</textarea>
|
||||||
|
<div id="main-config-status" class="json-status valid">VALID</div>
|
||||||
<div id="main-config-validation" class="json-validation"></div>
|
<div id="main-config-validation" class="json-validation"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="json-actions">
|
<div class="json-actions">
|
||||||
<button type="button" onclick="copyToClipboard('main-config-json')" style="margin-right: 10px;">Copy Main Config</button>
|
<button type="button" onclick="copyToClipboard('main-config-json')" style="margin-right: 10px;">Copy Main Config</button>
|
||||||
|
<button type="button" onclick="formatJson('main-config-json')" style="background-color: #FF9800; margin-right: 10px;">Format JSON</button>
|
||||||
<button type="button" onclick="validateJson('main-config-json', 'main-config-validation')" style="background-color: #2196F3;">Validate JSON</button>
|
<button type="button" onclick="validateJson('main-config-json', 'main-config-validation')" style="background-color: #2196F3;">Validate JSON</button>
|
||||||
|
<button type="button" onclick="previewChanges('main')" style="background-color: #9C27B0; margin-right: 10px;">Preview Changes</button>
|
||||||
|
<button type="button" onclick="saveRawJson('main')" class="save-button" style="margin-left: 10px;">Save Main Config</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1591,13 +1673,17 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Raw JSON:</label>
|
<label>Raw JSON:</label>
|
||||||
<div class="json-container">
|
<div class="json-container">
|
||||||
<textarea id="secrets-config-json" readonly style="height: 400px; font-family: monospace; font-size: 12px;">{{ secrets_config_json }}</textarea>
|
<textarea id="secrets-config-json" style="height: 400px; font-family: monospace; font-size: 12px;">{{ secrets_config_json }}</textarea>
|
||||||
|
<div id="secrets-config-status" class="json-status valid">VALID</div>
|
||||||
<div id="secrets-config-validation" class="json-validation"></div>
|
<div id="secrets-config-validation" class="json-validation"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="json-actions">
|
<div class="json-actions">
|
||||||
<button type="button" onclick="copyToClipboard('secrets-config-json')" style="margin-right: 10px;">Copy Secrets Config</button>
|
<button type="button" onclick="copyToClipboard('secrets-config-json')" style="margin-right: 10px;">Copy Secrets Config</button>
|
||||||
|
<button type="button" onclick="formatJson('secrets-config-json')" style="background-color: #FF9800; margin-right: 10px;">Format JSON</button>
|
||||||
<button type="button" onclick="validateJson('secrets-config-json', 'secrets-config-validation')" style="background-color: #2196F3;">Validate JSON</button>
|
<button type="button" onclick="validateJson('secrets-config-json', 'secrets-config-validation')" style="background-color: #2196F3;">Validate JSON</button>
|
||||||
|
<button type="button" onclick="previewChanges('secrets')" style="background-color: #9C27B0; margin-right: 10px;">Preview Changes</button>
|
||||||
|
<button type="button" onclick="saveRawJson('secrets')" class="save-button" style="margin-left: 10px;">Save Secrets Config</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -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) {
|
function validateJson(textareaId, validationId) {
|
||||||
const textarea = document.getElementById(textareaId);
|
const textarea = document.getElementById(textareaId);
|
||||||
const validationDiv = document.getElementById(validationId);
|
const validationDiv = document.getElementById(validationId);
|
||||||
@@ -1678,6 +1808,10 @@
|
|||||||
validationDiv.className = 'json-validation';
|
validationDiv.className = 'json-validation';
|
||||||
validationDiv.style.display = 'block';
|
validationDiv.style.display = 'block';
|
||||||
|
|
||||||
|
// Update status indicator
|
||||||
|
const statusId = validationId.replace('-validation', '-status');
|
||||||
|
const statusElement = document.getElementById(statusId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Try to parse the JSON
|
// Try to parse the JSON
|
||||||
const parsed = JSON.parse(jsonText);
|
const parsed = JSON.parse(jsonText);
|
||||||
@@ -1714,6 +1848,10 @@
|
|||||||
// Display results
|
// Display results
|
||||||
if (warnings.length > 0) {
|
if (warnings.length > 0) {
|
||||||
validationDiv.className = 'json-validation warning';
|
validationDiv.className = 'json-validation warning';
|
||||||
|
if (statusElement) {
|
||||||
|
statusElement.textContent = 'WARNING';
|
||||||
|
statusElement.className = 'json-status warning';
|
||||||
|
}
|
||||||
validationDiv.innerHTML = `
|
validationDiv.innerHTML = `
|
||||||
<div class="json-error-message">✅ JSON is valid but has warnings:</div>
|
<div class="json-error-message">✅ JSON is valid but has warnings:</div>
|
||||||
<div class="json-error-details">
|
<div class="json-error-details">
|
||||||
@@ -1722,6 +1860,10 @@
|
|||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
validationDiv.className = 'json-validation success';
|
validationDiv.className = 'json-validation success';
|
||||||
|
if (statusElement) {
|
||||||
|
statusElement.textContent = 'VALID';
|
||||||
|
statusElement.className = 'json-status valid';
|
||||||
|
}
|
||||||
validationDiv.innerHTML = `
|
validationDiv.innerHTML = `
|
||||||
<div class="json-error-message">✅ JSON is valid!</div>
|
<div class="json-error-message">✅ JSON is valid!</div>
|
||||||
<div class="json-error-details">
|
<div class="json-error-details">
|
||||||
@@ -1735,6 +1877,10 @@
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
// JSON parsing failed
|
// JSON parsing failed
|
||||||
validationDiv.className = 'json-validation error';
|
validationDiv.className = 'json-validation error';
|
||||||
|
if (statusElement) {
|
||||||
|
statusElement.textContent = 'INVALID';
|
||||||
|
statusElement.className = 'json-status error';
|
||||||
|
}
|
||||||
|
|
||||||
let errorMessage = '❌ Invalid JSON syntax';
|
let errorMessage = '❌ Invalid JSON syntax';
|
||||||
let errorDetails = error.message;
|
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 = `
|
||||||
|
<div class="json-error-message">✅ Configuration saved successfully!</div>
|
||||||
|
<div class="json-error-details">
|
||||||
|
✓ JSON is valid<br>
|
||||||
|
✓ File saved successfully<br>
|
||||||
|
✓ Configuration updated
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
validationDiv.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
showMessage(data.message, 'error');
|
||||||
|
// Show error in validation area
|
||||||
|
validationDiv.className = 'json-validation error';
|
||||||
|
validationDiv.innerHTML = `
|
||||||
|
<div class="json-error-message">❌ Save failed</div>
|
||||||
|
<div class="json-error-details">
|
||||||
|
<strong>Error:</strong> ${data.message}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
validationDiv.style.display = 'block';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
showMessage(`Error saving configuration: ${error}`, 'error');
|
||||||
|
// Show error in validation area
|
||||||
|
validationDiv.className = 'json-validation error';
|
||||||
|
validationDiv.innerHTML = `
|
||||||
|
<div class="json-error-message">❌ Save failed</div>
|
||||||
|
<div class="json-error-details">
|
||||||
|
<strong>Error:</strong> ${error}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
validationDiv.style.display = 'block';
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
// Restore button state
|
||||||
|
saveButton.textContent = originalText;
|
||||||
|
saveButton.disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function saveSportsConfig() {
|
function saveSportsConfig() {
|
||||||
// Collect all sports configuration and save
|
// Collect all sports configuration and save
|
||||||
const config = {
|
const config = {
|
||||||
@@ -2541,6 +2771,51 @@
|
|||||||
// Set default active tab
|
// Set default active tab
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
document.querySelector('.tab-link').click();
|
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');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -286,5 +286,47 @@ def run_action_route():
|
|||||||
'message': f'Error running action: {e}'
|
'message': f'Error running action: {e}'
|
||||||
}), 400
|
}), 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__':
|
if __name__ == '__main__':
|
||||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||||
Reference in New Issue
Block a user