mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 13:02:59 +00:00
adding scheduling function to web ui and display
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
{
|
||||
"web_display_autostart": false,
|
||||
"schedule": {
|
||||
"enabled": false,
|
||||
"start_time": "07:00",
|
||||
"end_time": "23:00"
|
||||
},
|
||||
"timezone": "America/Chicago",
|
||||
"location": {
|
||||
"city": "Dallas",
|
||||
|
||||
@@ -2,6 +2,7 @@ import time
|
||||
import logging
|
||||
import sys
|
||||
from typing import Dict, Any, List
|
||||
from datetime import datetime, time as time_obj
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
@@ -411,6 +412,12 @@ class DisplayController:
|
||||
logger.info(f"Initial display mode: {self.current_display_mode}")
|
||||
logger.info("DisplayController initialized with display_manager: %s", id(self.display_manager))
|
||||
|
||||
# --- SCHEDULING & CONFIG REFRESH ---
|
||||
self.config_check_interval = 30
|
||||
self.last_config_check = 0
|
||||
self.is_display_active = True
|
||||
self._load_config() # Initial load of schedule
|
||||
|
||||
def _handle_music_update(self, track_info: Dict[str, Any], significant_change: bool = False):
|
||||
"""Callback for when music track info changes."""
|
||||
# MusicManager now handles its own display state (album art, etc.)
|
||||
@@ -712,6 +719,44 @@ class DisplayController:
|
||||
self.ncaa_fb_current_team_index = (self.ncaa_fb_current_team_index + 1) % len(self.ncaa_fb_favorite_teams)
|
||||
self.ncaa_fb_showing_recent = True # Reset to recent for the new team
|
||||
|
||||
# --- SCHEDULING METHODS ---
|
||||
def _load_config(self):
|
||||
"""Load configuration from the config manager and parse schedule settings."""
|
||||
self.config = self.config_manager.load_config()
|
||||
schedule_config = self.config.get('schedule', {})
|
||||
self.schedule_enabled = schedule_config.get('enabled', False)
|
||||
try:
|
||||
self.start_time = datetime.strptime(schedule_config.get('start_time', '07:00'), '%H:%M').time()
|
||||
self.end_time = datetime.strptime(schedule_config.get('end_time', '22:00'), '%H:%M').time()
|
||||
except (ValueError, TypeError):
|
||||
logger.warning("Invalid time format in schedule config. Using defaults.")
|
||||
self.start_time = time_obj(7, 0)
|
||||
self.end_time = time_obj(22, 0)
|
||||
|
||||
def _check_schedule(self):
|
||||
"""Check if the display should be active based on the schedule."""
|
||||
if not self.schedule_enabled:
|
||||
if not self.is_display_active:
|
||||
logger.info("Schedule is disabled. Activating display.")
|
||||
self.is_display_active = True
|
||||
return
|
||||
|
||||
now_time = datetime.now().time()
|
||||
|
||||
# Handle overnight schedules
|
||||
if self.start_time <= self.end_time:
|
||||
should_be_active = self.start_time <= now_time < self.end_time
|
||||
else:
|
||||
should_be_active = now_time >= self.start_time or now_time < self.end_time
|
||||
|
||||
if should_be_active and not self.is_display_active:
|
||||
logger.info("Within scheduled time. Activating display.")
|
||||
self.is_display_active = True
|
||||
elif not should_be_active and self.is_display_active:
|
||||
logger.info("Outside of scheduled time. Deactivating display.")
|
||||
self.display_manager.clear()
|
||||
self.is_display_active = False
|
||||
|
||||
def run(self):
|
||||
"""Run the display controller, switching between displays."""
|
||||
if not self.available_modes:
|
||||
@@ -722,6 +767,17 @@ class DisplayController:
|
||||
try:
|
||||
while True:
|
||||
current_time = time.time()
|
||||
|
||||
# Periodically check for config changes
|
||||
if current_time - self.last_config_check > self.config_check_interval:
|
||||
self._load_config()
|
||||
self.last_config_check = current_time
|
||||
|
||||
# Enforce the schedule
|
||||
self._check_schedule()
|
||||
if not self.is_display_active:
|
||||
time.sleep(60)
|
||||
continue
|
||||
|
||||
# Update data for all modules first
|
||||
self._update_modules()
|
||||
@@ -1024,10 +1080,8 @@ class DisplayController:
|
||||
# Force clear on the next iteration after an error to be safe
|
||||
self.force_clear = True
|
||||
|
||||
|
||||
# Small sleep removed - updates/drawing should manage timing
|
||||
# time.sleep(self.update_interval)
|
||||
#time.sleep(self.update_interval) # Re-add the sleep
|
||||
# Add a short sleep to prevent high CPU usage
|
||||
time.sleep(0.1)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Display controller stopped by user")
|
||||
|
||||
189
templates/index.html
Normal file
189
templates/index.html
Normal file
@@ -0,0 +1,189 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>LED Matrix Config</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
|
||||
.container { max-width: 800px; margin: auto; background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 15px rgba(0,0,0,0.1); }
|
||||
h1, h2 { text-align: center; color: #333; }
|
||||
.tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #ccc;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.tab-link {
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
font-size: 16px;
|
||||
border-bottom: 3px solid transparent;
|
||||
transition: border-bottom 0.3s;
|
||||
}
|
||||
.tab-link.active {
|
||||
border-bottom: 3px solid #4CAF50;
|
||||
font-weight: bold;
|
||||
}
|
||||
.tab-content { display: none; }
|
||||
.form-group { margin-bottom: 15px; }
|
||||
label { display: block; margin-bottom: 5px; font-weight: bold; }
|
||||
input[type="time"], input[type="checkbox"] {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
button {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
padding: 12px 25px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-top: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
button:hover { background-color: #45a049; }
|
||||
.flash-messages { 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; }
|
||||
.flash-messages .error { background-color: #f8d7da; color: #721c24; }
|
||||
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;
|
||||
}
|
||||
.filepath { font-family: monospace; background-color: #eee; padding: 2px 5px; border-radius: 3px; font-size: 0.9em;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<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-link active" onclick="openTab(event, 'schedule')">Schedule</button>
|
||||
<button class="tab-link" onclick="openTab(event, 'main')">Main Config</button>
|
||||
<button class="tab-link" onclick="openTab(event, 'secrets')">Secrets Config</button>
|
||||
<button class="tab-link" onclick="openTab(event, 'actions')">Actions</button>
|
||||
</div>
|
||||
|
||||
<!-- Schedule Tab -->
|
||||
<div id="schedule" class="tab-content" style="display: block;">
|
||||
<h2>Display Schedule</h2>
|
||||
<p>Set the time for the display to be active. A restart is needed for changes to take effect.</p>
|
||||
<form action="{{ url_for('save_schedule_route') }}" method="POST">
|
||||
<div class="form-group">
|
||||
<label for="schedule_enabled">Enable Schedule:</label>
|
||||
<input type="checkbox" id="schedule_enabled" name="schedule_enabled" {% if schedule_config.enabled %}checked{% endif %}>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="start_time">Display On Time:</label>
|
||||
<input type="time" id="start_time" name="start_time" value="{{ schedule_config.start_time }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="end_time">Display Off Time:</label>
|
||||
<input type="time" id="end_time" name="end_time" value="{{ schedule_config.end_time }}">
|
||||
</div>
|
||||
<button type="submit">Save Schedule</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Main Config Tab -->
|
||||
<div id="main" class="tab-content">
|
||||
<form action="{{ url_for('save_config_route') }}" method="POST">
|
||||
<input type="hidden" name="config_type" value="main">
|
||||
<h2>Main Configuration (<span class="filepath">{{ main_config_path }}</span>)</h2>
|
||||
<textarea name="config_data">{{ main_config_json }}</textarea>
|
||||
<button type="submit">Save Main Config</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Secrets Tab -->
|
||||
<div id="secrets" class="tab-content">
|
||||
<form action="{{ url_for('save_config_route') }}" method="POST">
|
||||
<input type="hidden" name="config_type" value="secrets">
|
||||
<h2>Secrets Configuration (<span class="filepath">{{ secrets_config_path }}</span>)</h2>
|
||||
<textarea name="config_data">{{ secrets_config_json }}</textarea>
|
||||
<button type="submit">Save Secrets</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Actions Tab -->
|
||||
<div id="actions" class="tab-content">
|
||||
<h2>System Actions</h2>
|
||||
<p>Control the display service and system.</p>
|
||||
<div class="action-buttons">
|
||||
<button type="button" class="action-button" onclick="runAction('start_display')">Start Display</button>
|
||||
<button type="button" class="action-button" onclick="runAction('stop_display')">Stop Display</button>
|
||||
<hr>
|
||||
<button type="button" class="action-button" onclick="runAction('enable_autostart')">Enable Auto-Start</button>
|
||||
<button type="button" class="action-button" onclick="runAction('disable_autostart')">Disable Auto-Start</button>
|
||||
<hr>
|
||||
<button type="button" class="action-button" onclick="runAction('reboot_system')">Reboot System</button>
|
||||
<hr>
|
||||
<button type="button" class="action-button" onclick="runAction('git_pull')">Download Latest Update</button>
|
||||
</div>
|
||||
<div id="action_output_container" style="margin-top: 20px;">
|
||||
<h3>Action Output:</h3>
|
||||
<pre id="action_output">No action run yet.</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openTab(evt, tabName) {
|
||||
var i, tabcontent, tablinks;
|
||||
tabcontent = document.getElementsByClassName("tab-content");
|
||||
for (i = 0; i < tabcontent.length; i++) {
|
||||
tabcontent[i].style.display = "none";
|
||||
}
|
||||
tablinks = document.getElementsByClassName("tab-link");
|
||||
for (i = 0; i < tablinks.length; i++) {
|
||||
tablinks[i].className = tablinks[i].className.replace(" active", "");
|
||||
}
|
||||
document.getElementById(tabName).style.display = "block";
|
||||
evt.currentTarget.className += " active";
|
||||
}
|
||||
|
||||
function runAction(actionName) {
|
||||
const outputElement = document.getElementById('action_output');
|
||||
outputElement.textContent = `Running ${actionName.replace(/_/g, ' ')}...`;
|
||||
|
||||
fetch("{{ url_for('run_action_route') }}", {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: actionName })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
let outputText = `Status: ${data.status}\nMessage: ${data.message}\n`;
|
||||
if (data.stdout) outputText += `\n--- STDOUT ---\n${data.stdout}`;
|
||||
if (data.stderr) outputText += `\n--- STDERR ---\n${data.stderr}`;
|
||||
outputElement.textContent = outputText;
|
||||
})
|
||||
.catch(error => {
|
||||
outputElement.textContent = `Error: ${error}`;
|
||||
});
|
||||
}
|
||||
// Set default active tab
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
document.querySelector('.tab-link').click();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
450
web_interface.py
450
web_interface.py
@@ -1,410 +1,108 @@
|
||||
from flask import Flask, render_template_string, request, redirect, url_for, flash, jsonify
|
||||
from flask import Flask, render_template, request, redirect, url_for, flash, jsonify
|
||||
import json
|
||||
import os # Added os import
|
||||
import subprocess # Added subprocess import
|
||||
import os
|
||||
import subprocess
|
||||
from src.config_manager import ConfigManager
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = os.urandom(24) # Needed for flash messages
|
||||
app.secret_key = os.urandom(24)
|
||||
config_manager = ConfigManager()
|
||||
|
||||
CONFIG_PAGE_TEMPLATE = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>LED Matrix Config</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
|
||||
.container { max-width: 800px; margin: auto; background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 0 15px rgba(0,0,0,0.1); }
|
||||
h1 { text-align: center; color: #333; }
|
||||
.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; }
|
||||
.flash-messages {
|
||||
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>
|
||||
<!-- 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>
|
||||
<body>
|
||||
<div class="container">
|
||||
<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')" data-tab="main">Main Config</button>
|
||||
<button class="tab-button {% if active_tab == 'secrets' %}active{% endif %}" onclick="openTab('secrets')" data-tab="secrets">Secrets Config</button>
|
||||
<button class="tab-button {% if active_tab == 'actions' %}active{% endif %}" onclick="openTab('actions')" data-tab="actions">Actions</button>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ url_for('save_config_route') }}">
|
||||
<input type="hidden" name="config_type" id="config_type_hidden" value="{{ active_tab }}">
|
||||
|
||||
<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>
|
||||
|
||||
<div id="actions" class="tab-content {% if active_tab == 'actions' %}active{% endif %}">
|
||||
<h2>Display & Service Actions</h2>
|
||||
<div class="action-buttons">
|
||||
<button type="button" class="action-button" onclick="runAction('start_display')">Start Display</button>
|
||||
<button type="button" class="action-button" onclick="runAction('stop_display')">Stop Display</button>
|
||||
<hr>
|
||||
<button type="button" class="action-button" onclick="runAction('enable_autostart')">Enable Auto-Start</button>
|
||||
<button type="button" class="action-button" onclick="runAction('disable_autostart')">Disable Auto-Start</button>
|
||||
<hr>
|
||||
<button type="button" class="action-button" onclick="runAction('reboot_system')">Reboot System</button>
|
||||
<hr>
|
||||
<button type="button" class="action-button" onclick="runAction('git_pull')">Download Latest Update</button>
|
||||
</div>
|
||||
<div id="action_output_container" style="margin-top: 20px;">
|
||||
<h3>Action Output:</h3>
|
||||
<pre id="action_output" style="background-color: #333; color: #fff; padding: 15px; border-radius: 4px; min-height: 100px; max-height: 300px; overflow-y: auto; white-space: pre-wrap; word-wrap: break-word;">No action run yet.</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="submit" value="Save Current Tab's Configuration" id="save_config_button">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var mainEditor = null; // Declare editors in a scope accessible to openTab
|
||||
var secretsEditor = null;
|
||||
|
||||
function openTab(tabName) {
|
||||
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].classList.remove("active");
|
||||
}
|
||||
tabbuttons = document.getElementsByClassName("tab-button");
|
||||
for (i = 0; i < tabbuttons.length; i++) {
|
||||
tabbuttons[i].classList.remove("active");
|
||||
}
|
||||
|
||||
document.getElementById(tabName).classList.add("active");
|
||||
document.querySelector(".tab-button[data-tab='" + tabName + "']").classList.add("active");
|
||||
document.getElementById("config_type_hidden").value = tabName;
|
||||
|
||||
// Refresh the corresponding CodeMirror instance
|
||||
if (tabName === 'main' && mainEditor) {
|
||||
mainEditor.refresh();
|
||||
}
|
||||
if (tabName === 'secrets' && secretsEditor) {
|
||||
secretsEditor.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize CodeMirror for the main config textarea
|
||||
var mainConfigTextArea = document.querySelector("textarea[name='main_config_data']");
|
||||
if (mainConfigTextArea) {
|
||||
mainEditor = CodeMirror.fromTextArea(mainConfigTextArea, {
|
||||
lineNumbers: true,
|
||||
mode: {name: "javascript", json: true},
|
||||
theme: "material-palenight",
|
||||
gutters: ["CodeMirror-lint-markers"],
|
||||
lint: true
|
||||
});
|
||||
new MutationObserver(() => mainEditor.refresh()).observe(document.getElementById('main'), {attributes: true, childList: false, subtree: false});
|
||||
}
|
||||
|
||||
// Initialize CodeMirror for the secrets config textarea
|
||||
var secretsConfigTextArea = document.querySelector("textarea[name='secrets_config_data']");
|
||||
if (secretsConfigTextArea) {
|
||||
secretsEditor = CodeMirror.fromTextArea(secretsConfigTextArea, {
|
||||
lineNumbers: true,
|
||||
mode: {name: "javascript", json: true},
|
||||
theme: "material-palenight",
|
||||
gutters: ["CodeMirror-lint-markers"],
|
||||
lint: true
|
||||
});
|
||||
new MutationObserver(() => secretsEditor.refresh()).observe(document.getElementById('secrets'), {attributes: true, childList: false, subtree: false});
|
||||
}
|
||||
|
||||
// Ensure CodeMirror instances save their content back to textareas before form submission
|
||||
const form = document.querySelector('form');
|
||||
if (form) {
|
||||
form.addEventListener('submit', function() {
|
||||
if (mainEditor) {
|
||||
mainEditor.save();
|
||||
}
|
||||
if (secretsEditor) {
|
||||
secretsEditor.save();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initial tab setup from URL or default
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const initialTab = params.get('tab') || 'main';
|
||||
openTab(initialTab);
|
||||
});
|
||||
|
||||
function runAction(actionName) {
|
||||
const outputElement = document.getElementById('action_output');
|
||||
outputElement.textContent = `Running ${actionName.replace('_', ' ')}...`;
|
||||
|
||||
// Disable buttons during action
|
||||
document.querySelectorAll('.action-button').forEach(button => button.disabled = true);
|
||||
|
||||
fetch("{{ url_for('run_action_route') }}", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ action: actionName })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
let outputText = `Action: ${actionName}\nStatus: ${data.status}\nMessage: ${data.message}\n`;
|
||||
if (data.stdout) {
|
||||
outputText += `\nStdout:\n${data.stdout}`;
|
||||
}
|
||||
if (data.stderr) {
|
||||
outputText += `\nStderr:\n${data.stderr}`;
|
||||
}
|
||||
if (data.error) {
|
||||
outputText += `\nError: ${data.error}`;
|
||||
}
|
||||
outputElement.textContent = outputText;
|
||||
flash(data.message, data.status);
|
||||
})
|
||||
.catch(error => {
|
||||
outputElement.textContent = `Error running action ${actionName}: ${error}`;
|
||||
flash(`Client-side error running action: ${error}`, 'error');
|
||||
})
|
||||
.finally(() => {
|
||||
// Re-enable buttons
|
||||
document.querySelectorAll('.action-button').forEach(button => button.disabled = false);
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to show flash messages dynamically (optional)
|
||||
function flash(message, category) {
|
||||
const flashContainer = document.querySelector('.flash-messages') || (() => {
|
||||
const container = document.createElement('ul');
|
||||
container.className = 'flash-messages';
|
||||
document.querySelector('.container').prepend(container);
|
||||
return container;
|
||||
})();
|
||||
const listItem = document.createElement('li');
|
||||
listItem.className = category;
|
||||
listItem.textContent = message;
|
||||
flashContainer.appendChild(listItem);
|
||||
setTimeout(() => listItem.remove(), 5000); // Remove after 5 seconds
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- CodeMirror JS (Corrected Order) -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsonlint/1.6.3/jsonlint.min.js"></script> <!-- Defines global jsonlint -->
|
||||
<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> <!-- Uses global jsonlint -->
|
||||
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
@app.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
|
||||
|
||||
def index():
|
||||
try:
|
||||
main_config = config_manager.load_config()
|
||||
schedule_config = main_config.get('schedule', {})
|
||||
|
||||
main_config_data = config_manager.get_raw_file_content('main')
|
||||
except Exception as e:
|
||||
flash(f"Error loading main config: {str(e)}", "error")
|
||||
|
||||
try:
|
||||
secrets_config_data = config_manager.get_raw_file_content('secrets')
|
||||
main_config_json = json.dumps(main_config_data, indent=4)
|
||||
secrets_config_json = json.dumps(secrets_config_data, indent=4)
|
||||
|
||||
except Exception as e:
|
||||
flash(f"Error loading secrets config: {str(e)}", "error")
|
||||
flash(f"Error loading configuration: {e}", "error")
|
||||
schedule_config = {}
|
||||
main_config_json = "{}"
|
||||
secrets_config_json = "{}"
|
||||
|
||||
main_config_json = json.dumps(main_config_data, indent=4)
|
||||
secrets_config_json = json.dumps(secrets_config_data, indent=4)
|
||||
|
||||
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'])
|
||||
def save_config_route():
|
||||
config_type = request.form.get('config_type', 'main')
|
||||
data_to_save_str = ""
|
||||
|
||||
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 render_template('index.html',
|
||||
schedule_config=schedule_config,
|
||||
main_config_json=main_config_json,
|
||||
secrets_config_json=secrets_config_json,
|
||||
main_config_path=config_manager.get_config_path(),
|
||||
secrets_config_path=config_manager.get_secrets_path())
|
||||
|
||||
@app.route('/save_schedule', methods=['POST'])
|
||||
def save_schedule_route():
|
||||
try:
|
||||
new_data = json.loads(data_to_save_str)
|
||||
main_config = config_manager.load_config()
|
||||
|
||||
schedule_data = {
|
||||
'enabled': 'schedule_enabled' in request.form,
|
||||
'start_time': request.form.get('start_time', '07:00'),
|
||||
'end_time': request.form.get('end_time', '22:00')
|
||||
}
|
||||
|
||||
main_config['schedule'] = schedule_data
|
||||
config_manager.save_config(main_config)
|
||||
|
||||
flash("Schedule updated successfully! Restart the display for changes to take effect.", "success")
|
||||
|
||||
except Exception as e:
|
||||
flash(f"Error saving schedule: {e}", "error")
|
||||
|
||||
return redirect(url_for('index'))
|
||||
|
||||
@app.route('/save_config', methods=['POST'])
|
||||
def save_config_route():
|
||||
config_type = request.form.get('config_type')
|
||||
config_data_str = request.form.get('config_data')
|
||||
|
||||
try:
|
||||
new_data = json.loads(config_data_str)
|
||||
config_manager.save_raw_file_content(config_type, new_data)
|
||||
flash(f"{config_type.capitalize()} configuration saved successfully!", "success")
|
||||
except json.JSONDecodeError:
|
||||
flash(f"Error: Invalid JSON format submitted for {config_type} config.", "error")
|
||||
flash(f"Error: Invalid JSON format for {config_type} config.", "error")
|
||||
except Exception as e:
|
||||
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
|
||||
flash(f"Error saving {config_type} configuration: {e}", "error")
|
||||
|
||||
return redirect(url_for('index'))
|
||||
|
||||
@app.route('/run_action', methods=['POST'])
|
||||
def run_action_route():
|
||||
data = request.get_json()
|
||||
action = data.get('action')
|
||||
command_parts = [] # Use a list for subprocess
|
||||
explanation_msg = ""
|
||||
|
||||
commands = {
|
||||
'start_display': ["sudo", "python3", "display_controller.py"],
|
||||
'stop_display': ["sudo", "pkill", "-f", "display_controller.py"],
|
||||
'enable_autostart': ["sudo", "systemctl", "enable", "ledmatrix.service"],
|
||||
'disable_autostart': ["sudo", "systemctl", "disable", "ledmatrix.service"],
|
||||
'reboot_system': ["sudo", "reboot"],
|
||||
'git_pull': ["git", "pull"]
|
||||
}
|
||||
|
||||
if action == 'start_display':
|
||||
command_parts = ["sudo", "python", "display_controller.py"] # Changed command
|
||||
explanation_msg = "Starts the LED matrix display by directly running display_controller.py with sudo."
|
||||
elif action == 'stop_display':
|
||||
command_parts = ["bash", "stop_display.sh"]
|
||||
explanation_msg = "Stops the LED matrix display by executing the stop_display.sh script."
|
||||
elif action == 'enable_autostart':
|
||||
command_parts = ["sudo", "systemctl", "enable", "ledmatrix.service"]
|
||||
explanation_msg = "Enables the LED matrix service to start automatically on boot."
|
||||
elif action == 'disable_autostart':
|
||||
command_parts = ["sudo", "systemctl", "disable", "ledmatrix.service"]
|
||||
explanation_msg = "Disables the LED matrix service from starting automatically on boot."
|
||||
elif action == 'reboot_system':
|
||||
command_parts = ["sudo", "reboot"]
|
||||
explanation_msg = "Reboots the system."
|
||||
elif action == 'git_pull':
|
||||
command_parts = ["git", "pull"]
|
||||
explanation_msg = "Downloads the latest updates from the repository."
|
||||
else:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": "Invalid action specified.",
|
||||
"error": "Unknown action"
|
||||
}), 400
|
||||
command_parts = commands.get(action)
|
||||
|
||||
if not command_parts:
|
||||
return jsonify({"status": "error", "message": "Invalid action."}), 400
|
||||
|
||||
try:
|
||||
# Direct execution using subprocess
|
||||
process_result = subprocess.run(command_parts, capture_output=True, text=True, check=False)
|
||||
result = subprocess.run(command_parts, capture_output=True, text=True, check=False)
|
||||
|
||||
status = "success" if result.returncode == 0 else "error"
|
||||
message = f"Action '{action}' completed."
|
||||
|
||||
stdout_content = process_result.stdout
|
||||
stderr_content = process_result.stderr
|
||||
exit_code = process_result.returncode
|
||||
|
||||
current_status = "success"
|
||||
message_to_user = f"Action '{action}' executed. Exit code: {exit_code}."
|
||||
|
||||
if exit_code != 0:
|
||||
current_status = "error"
|
||||
message_to_user = f"Action '{action}' failed. Exit code: {exit_code}."
|
||||
elif stderr_content: # Even with exit code 0, stderr might contain warnings
|
||||
current_status = "warning"
|
||||
message_to_user = f"Action '{action}' executed with output in stderr (Exit code: {exit_code})."
|
||||
|
||||
return jsonify({
|
||||
"status": current_status,
|
||||
"message": message_to_user,
|
||||
"stdout": stdout_content,
|
||||
"stderr": stderr_content
|
||||
"status": status,
|
||||
"message": message,
|
||||
"stdout": result.stdout,
|
||||
"stderr": result.stderr
|
||||
})
|
||||
|
||||
except FileNotFoundError as e:
|
||||
# This occurs if the command itself (e.g., 'bash', 'sudo', or the script) isn't found
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"Error executing action '{action}': Command or script not found.",
|
||||
"error": str(e),
|
||||
"stdout": "",
|
||||
"stderr": f"Command not found: {command_parts[0] if command_parts else ''}"
|
||||
}), 500
|
||||
except Exception as e:
|
||||
# Catch any other exceptions during subprocess execution or in this route
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"An unexpected error occurred while processing action: {action}",
|
||||
"error": str(e)
|
||||
}), 500
|
||||
return jsonify({"status": "error", "message": str(e)}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||
Reference in New Issue
Block a user