mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-11 21:33:00 +00:00
json formatting and optional config secrets editor
This commit is contained in:
@@ -9,6 +9,12 @@ class ConfigManager:
|
|||||||
self.secrets_path = secrets_path or "config/config_secrets.json"
|
self.secrets_path = secrets_path or "config/config_secrets.json"
|
||||||
self.config: Dict[str, Any] = {}
|
self.config: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
def get_config_path(self) -> str:
|
||||||
|
return self.config_path
|
||||||
|
|
||||||
|
def get_secrets_path(self) -> str:
|
||||||
|
return self.secrets_path
|
||||||
|
|
||||||
def load_config(self) -> Dict[str, Any]:
|
def load_config(self) -> Dict[str, Any]:
|
||||||
"""Load configuration from JSON files."""
|
"""Load configuration from JSON files."""
|
||||||
try:
|
try:
|
||||||
@@ -106,3 +112,59 @@ class ConfigManager:
|
|||||||
def get_clock_config(self) -> Dict[str, Any]:
|
def get_clock_config(self) -> Dict[str, Any]:
|
||||||
"""Get clock configuration."""
|
"""Get clock configuration."""
|
||||||
return self.config.get('clock', {})
|
return self.config.get('clock', {})
|
||||||
|
|
||||||
|
def get_raw_file_content(self, file_type: str) -> Dict[str, Any]:
|
||||||
|
"""Load raw content of 'main' config or 'secrets' config file."""
|
||||||
|
path_to_load = ""
|
||||||
|
if file_type == "main":
|
||||||
|
path_to_load = self.config_path
|
||||||
|
elif file_type == "secrets":
|
||||||
|
path_to_load = self.secrets_path
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid file_type specified. Must be 'main' or 'secrets'.")
|
||||||
|
|
||||||
|
if not os.path.exists(path_to_load):
|
||||||
|
# If a secrets file doesn't exist, it's not an error, just return empty
|
||||||
|
if file_type == "secrets":
|
||||||
|
return {}
|
||||||
|
print(f"{file_type.capitalize()} configuration file not found at {os.path.abspath(path_to_load)}")
|
||||||
|
raise FileNotFoundError(f"{file_type.capitalize()} configuration file not found at {os.path.abspath(path_to_load)}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(path_to_load, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(f"Error parsing {file_type} configuration file: {path_to_load}")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading {file_type} configuration file {path_to_load}: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def save_raw_file_content(self, file_type: str, data: Dict[str, Any]) -> None:
|
||||||
|
"""Save data directly to 'main' config or 'secrets' config file."""
|
||||||
|
path_to_save = ""
|
||||||
|
if file_type == "main":
|
||||||
|
path_to_save = self.config_path
|
||||||
|
elif file_type == "secrets":
|
||||||
|
path_to_save = self.secrets_path
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid file_type specified. Must be 'main' or 'secrets'.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create directory if it doesn't exist, especially for config/
|
||||||
|
os.makedirs(os.path.dirname(path_to_save), exist_ok=True)
|
||||||
|
with open(path_to_save, 'w') as f:
|
||||||
|
json.dump(data, f, indent=4)
|
||||||
|
print(f"{file_type.capitalize()} configuration successfully saved to {os.path.abspath(path_to_save)}")
|
||||||
|
|
||||||
|
# If we just saved the main config or secrets, the merged self.config might be stale.
|
||||||
|
# Reload it to reflect the new state.
|
||||||
|
if file_type == "main" or file_type == "secrets":
|
||||||
|
self.load_config()
|
||||||
|
|
||||||
|
except IOError as e:
|
||||||
|
print(f"Error writing {file_type} configuration to file {os.path.abspath(path_to_save)}: {e}")
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An unexpected error occurred while saving {file_type} configuration: {str(e)}")
|
||||||
|
raise
|
||||||
247
web_interface.py
247
web_interface.py
@@ -1,62 +1,259 @@
|
|||||||
from flask import Flask, render_template_string, request, redirect, url_for
|
from flask import Flask, render_template_string, request, redirect, url_for, flash
|
||||||
import json # Added import for json
|
import json
|
||||||
|
import os # Added os import
|
||||||
from src.config_manager import ConfigManager
|
from src.config_manager import ConfigManager
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
app.secret_key = os.urandom(24) # Needed for flash messages
|
||||||
config_manager = ConfigManager()
|
config_manager = ConfigManager()
|
||||||
|
|
||||||
CONFIG_TEMPLATE = """
|
CONFIG_PAGE_TEMPLATE = """
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>LED Matrix Config</title>
|
<title>LED Matrix Config</title>
|
||||||
<style>
|
<style>
|
||||||
body { font-family: sans-serif; margin: 20px; }
|
body { font-family: sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
|
||||||
label { display: block; margin-top: 10px; }
|
.container { max-width: 800px; margin: auto; background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 15px rgba(0,0,0,0.1); }
|
||||||
input[type="text"], textarea { width: 100%; padding: 8px; margin-top: 5px; border-radius: 4px; border: 1px solid #ccc; box-sizing: border-box; }
|
h1 { text-align: center; color: #333; }
|
||||||
input[type="submit"] { background-color: #4CAF50; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; margin-top: 20px;}
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.tab-button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
font-size: 16px;
|
||||||
|
border-bottom: 3px solid transparent;
|
||||||
|
}
|
||||||
|
.tab-button.active {
|
||||||
|
border-bottom: 3px solid #4CAF50;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.tab-content { display: none; }
|
||||||
|
.tab-content.active { display: block; }
|
||||||
|
label { display: block; margin-top: 10px; font-weight: bold; }
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
margin-top: 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: monospace;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
input[type="submit"] {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 25px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
input[type="submit"]:hover { background-color: #45a049; }
|
input[type="submit"]:hover { background-color: #45a049; }
|
||||||
.container { max-width: 600px; margin: auto; background: #f9f9f9; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
|
.flash-messages {
|
||||||
h1 { text-align: center; }
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.flash-messages li {
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.flash-messages .success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
|
||||||
|
.flash-messages .error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
|
||||||
|
.filepath { font-family: monospace; background-color: #eee; padding: 2px 5px; border-radius: 3px; font-size: 0.9em;}
|
||||||
|
/* CodeMirror styling */
|
||||||
|
.CodeMirror { border: 1px solid #ccc; border-radius: 4px; min-height: 300px; font-family: monospace; font-size: 14px; }
|
||||||
</style>
|
</style>
|
||||||
|
<!-- CodeMirror CSS -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/theme/material-palenight.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/lint/lint.min.css">
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>LED Matrix Configuration</h1>
|
<h1>LED Matrix Configuration</h1>
|
||||||
|
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
<ul class="flash-messages">
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<li class="{{ category }}">{{ message }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<button class="tab-button {% if active_tab == 'main' %}active{% endif %}" onclick="openTab('main')">Main Config</button>
|
||||||
|
<button class="tab-button {% if active_tab == 'secrets' %}active{% endif %}" onclick="openTab('secrets')">Secrets Config</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form method="post" action="{{ url_for('save_config_route') }}">
|
<form method="post" action="{{ url_for('save_config_route') }}">
|
||||||
<label for="config_data">Configuration (JSON):</label>
|
<input type="hidden" name="config_type" id="config_type_hidden" value="{{ active_tab }}">
|
||||||
<textarea name="config_data" rows="20" cols="80">{{ config_json }}</textarea><br>
|
|
||||||
<input type="submit" value="Save Configuration">
|
<div id="main" class="tab-content {% if active_tab == 'main' %}active{% endif %}">
|
||||||
|
<h2>Main Configuration (<span class="filepath">{{ main_config_path }}</span>)</h2>
|
||||||
|
<label for="main_config_data">Edit {{ main_config_path }}:</label>
|
||||||
|
<textarea name="main_config_data" rows="25">{{ main_config_json }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="secrets" class="tab-content {% if active_tab == 'secrets' %}active{% endif %}">
|
||||||
|
<h2>Secrets Configuration (<span class="filepath">{{ secrets_config_path }}</span>)</h2>
|
||||||
|
<label for="secrets_config_data">Edit {{ secrets_config_path }}:</label>
|
||||||
|
<textarea name="secrets_config_data" rows="25">{{ secrets_config_json }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="submit" value="Save Current Tab's Configuration">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function openTab(tabName) {
|
||||||
|
// Update URL without reloading page for better UX
|
||||||
|
history.pushState(null, null, '{{ url_for("display_config_route") }}?tab=' + tabName);
|
||||||
|
|
||||||
|
var i, tabcontent, tabbuttons;
|
||||||
|
tabcontent = document.getElementsByClassName("tab-content");
|
||||||
|
for (i = 0; i < tabcontent.length; i++) {
|
||||||
|
tabcontent[i].style.display = "none";
|
||||||
|
tabcontent[i].classList.remove("active");
|
||||||
|
}
|
||||||
|
tabbuttons = document.getElementsByClassName("tab-button");
|
||||||
|
for (i = 0; i < tabbuttons.length; i++) {
|
||||||
|
tabbuttons[i].classList.remove("active");
|
||||||
|
}
|
||||||
|
document.getElementById(tabName).style.display = "block";
|
||||||
|
document.getElementById(tabName).classList.add("active");
|
||||||
|
// Set the active class for the button
|
||||||
|
document.querySelector(".tab-button[onclick=\"openTab('" + tabName + "')\"]").classList.add("active");
|
||||||
|
// Update hidden input for form submission
|
||||||
|
document.getElementById("config_type_hidden").value = tabName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the correct tab is active on page load based on URL parameter
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const tab = params.get('tab') || 'main'; // Default to main tab
|
||||||
|
openTab(tab);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- CodeMirror JS -->
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/javascript/javascript.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/lint/lint.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/lint/json-lint.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsonlint/1.6.3/jsonlint.min.js"></script> <!-- jsonlint.js dependency for json-lint addon -->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Initialize CodeMirror for the main config textarea
|
||||||
|
var mainConfigTextArea = document.querySelector("textarea[name='main_config_data']");
|
||||||
|
if (mainConfigTextArea) {
|
||||||
|
var mainEditor = CodeMirror.fromTextArea(mainConfigTextArea, {
|
||||||
|
lineNumbers: true,
|
||||||
|
mode: {name: "javascript", json: true}, // Use javascript mode with json flag
|
||||||
|
theme: "material-palenight",
|
||||||
|
gutters: ["CodeMirror-lint-markers"],
|
||||||
|
lint: true
|
||||||
|
});
|
||||||
|
// Refresh CodeMirror instance if it's in a tab that becomes visible later
|
||||||
|
new MutationObserver(() => mainEditor.refresh()).observe(document.getElementById('main'), {attributes: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize CodeMirror for the secrets config textarea
|
||||||
|
var secretsConfigTextArea = document.querySelector("textarea[name='secrets_config_data']");
|
||||||
|
if (secretsConfigTextArea) {
|
||||||
|
var secretsEditor = CodeMirror.fromTextArea(secretsConfigTextArea, {
|
||||||
|
lineNumbers: true,
|
||||||
|
mode: {name: "javascript", json: true}, // Use javascript mode with json flag
|
||||||
|
theme: "material-palenight",
|
||||||
|
gutters: ["CodeMirror-lint-markers"],
|
||||||
|
lint: true
|
||||||
|
});
|
||||||
|
// Refresh CodeMirror instance if it's in a tab that becomes visible later
|
||||||
|
new MutationObserver(() => secretsEditor.refresh()).observe(document.getElementById('secrets'), {attributes: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure CodeMirror instances save their content back to textareas before form submission
|
||||||
|
const form = document.querySelector('form');
|
||||||
|
if (form) {
|
||||||
|
form.addEventListener('submit', function() {
|
||||||
|
if (typeof mainEditor !== 'undefined' && mainEditor) {
|
||||||
|
mainEditor.save();
|
||||||
|
}
|
||||||
|
if (typeof secretsEditor !== 'undefined' && secretsEditor) {
|
||||||
|
secretsEditor.save();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def display_config_route():
|
def display_config_route():
|
||||||
|
active_tab = request.args.get('tab', 'main') # Default to 'main' tab
|
||||||
|
main_config_data = {}
|
||||||
|
secrets_config_data = {}
|
||||||
|
error_message = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
current_config = config_manager.load_config()
|
main_config_data = config_manager.get_raw_file_content('main')
|
||||||
# Pretty print JSON for the textarea
|
|
||||||
config_json = json.dumps(current_config, indent=4)
|
|
||||||
return render_template_string(CONFIG_TEMPLATE, config_json=config_json)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error loading configuration: {str(e)}", 500
|
flash(f"Error loading main config: {str(e)}", "error")
|
||||||
|
|
||||||
|
try:
|
||||||
|
secrets_config_data = config_manager.get_raw_file_content('secrets')
|
||||||
|
except Exception as e:
|
||||||
|
flash(f"Error loading secrets config: {str(e)}", "error")
|
||||||
|
|
||||||
|
main_config_json = json.dumps(main_config_data, indent=4, sort_keys=True)
|
||||||
|
secrets_config_json = json.dumps(secrets_config_data, indent=4, sort_keys=True)
|
||||||
|
|
||||||
|
return render_template_string(CONFIG_PAGE_TEMPLATE,
|
||||||
|
main_config_json=main_config_json,
|
||||||
|
secrets_config_json=secrets_config_json,
|
||||||
|
active_tab=active_tab,
|
||||||
|
main_config_path=config_manager.get_config_path(),
|
||||||
|
secrets_config_path=config_manager.get_secrets_path())
|
||||||
|
|
||||||
@app.route('/save', methods=['POST'])
|
@app.route('/save', methods=['POST'])
|
||||||
def save_config_route():
|
def save_config_route():
|
||||||
try:
|
config_type = request.form.get('config_type', 'main')
|
||||||
new_config_str = request.form['config_data']
|
data_to_save_str = ""
|
||||||
new_config = json.loads(new_config_str) # Parse the JSON string from textarea
|
|
||||||
config_manager.save_config(new_config)
|
if config_type == 'main':
|
||||||
|
data_to_save_str = request.form['main_config_data']
|
||||||
|
elif config_type == 'secrets':
|
||||||
|
data_to_save_str = request.form['secrets_config_data']
|
||||||
|
else:
|
||||||
|
flash("Invalid configuration type specified for saving.", "error")
|
||||||
return redirect(url_for('display_config_route'))
|
return redirect(url_for('display_config_route'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
new_data = json.loads(data_to_save_str)
|
||||||
|
config_manager.save_raw_file_content(config_type, new_data)
|
||||||
|
flash(f"{config_type.capitalize()} configuration saved successfully!", "success")
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return "Error: Invalid JSON format submitted.", 400
|
flash(f"Error: Invalid JSON format submitted for {config_type} config.", "error")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error saving configuration: {str(e)}", 500
|
flash(f"Error saving {config_type} configuration: {str(e)}", "error")
|
||||||
|
|
||||||
|
return redirect(url_for('display_config_route', tab=config_type)) # Redirect back to the same tab
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# Make sure to run with debug=True only for development
|
|
||||||
# In a production environment, use a proper WSGI server like Gunicorn
|
|
||||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||||
Reference in New Issue
Block a user