mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-06-20 11:38:37 +00:00
Compare commits
13 Commits
fix/wifi-a
...
909db0993f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
909db0993f | ||
|
|
1d2303e620 | ||
|
|
8652aacf37 | ||
|
|
76507014ce | ||
|
|
53806da8c5 | ||
|
|
3d4de89fd5 | ||
|
|
505fed70e3 | ||
|
|
c8d2eaeb85 | ||
|
|
745ba8101e | ||
|
|
ddc53ff1e0 | ||
|
|
2cd3dbabe5 | ||
|
|
f4e7fea7bb | ||
|
|
a5c7ef20ec |
@@ -190,7 +190,7 @@ class DisplayManager:
|
|||||||
json.dump(_hw_status, _f)
|
json.dump(_hw_status, _f)
|
||||||
_f.flush()
|
_f.flush()
|
||||||
os.fsync(_f.fileno())
|
os.fsync(_f.fileno())
|
||||||
os.chmod(_tmp_path, 0o600)
|
os.chmod(_tmp_path, 0o644)
|
||||||
os.replace(_tmp_path, _status_path)
|
os.replace(_tmp_path, _status_path)
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -147,8 +147,19 @@ class PluginLoader:
|
|||||||
if not requirements_file.exists():
|
if not requirements_file.exists():
|
||||||
return True # No dependencies needed
|
return True # No dependencies needed
|
||||||
|
|
||||||
|
# Resolve and validate plugin_dir before constructing derived paths from it
|
||||||
|
try:
|
||||||
|
plugin_dir_resolved = plugin_dir.resolve(strict=True)
|
||||||
|
except OSError:
|
||||||
|
self.logger.error("Plugin directory does not exist: %s", plugin_dir)
|
||||||
|
return False
|
||||||
|
marker_path = plugin_dir_resolved / ".dependencies_installed"
|
||||||
|
try:
|
||||||
|
marker_path.relative_to(plugin_dir_resolved)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
# Check if already installed
|
# Check if already installed
|
||||||
marker_path = plugin_dir / ".dependencies_installed"
|
|
||||||
if marker_path.exists():
|
if marker_path.exists():
|
||||||
self.logger.debug("Dependencies already installed for %s", plugin_id)
|
self.logger.debug("Dependencies already installed for %s", plugin_id)
|
||||||
return True
|
return True
|
||||||
@@ -171,10 +182,24 @@ class PluginLoader:
|
|||||||
self.logger.info("Dependencies installed successfully for %s", plugin_id)
|
self.logger.info("Dependencies installed successfully for %s", plugin_id)
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
stderr = result.stderr or ""
|
||||||
|
# uninstall-no-record-file means the package is already present at the
|
||||||
|
# system level (e.g. installed via dnf/apt without a pip RECORD file).
|
||||||
|
# pip can't replace it, but it IS installed — write the marker so we
|
||||||
|
# don't retry on every restart.
|
||||||
|
if "uninstall-no-record-file" in stderr:
|
||||||
|
self.logger.warning(
|
||||||
|
"Dependencies for %s include system-managed packages (no pip RECORD). "
|
||||||
|
"Assuming they are satisfied: %s",
|
||||||
|
plugin_id, stderr.strip()
|
||||||
|
)
|
||||||
|
marker_path.touch()
|
||||||
|
ensure_file_permissions(marker_path, get_plugin_file_mode())
|
||||||
|
return True
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
"Dependency installation returned non-zero exit code for %s: %s",
|
"Dependency installation returned non-zero exit code for %s: %s",
|
||||||
plugin_id,
|
plugin_id,
|
||||||
result.stderr
|
stderr
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ from pathlib import Path
|
|||||||
from typing import List, Dict, Optional, Any, Tuple
|
from typing import List, Dict, Optional, Any, Tuple
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from src.common.permission_utils import sudo_remove_directory
|
from src.common.permission_utils import sudo_remove_directory
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -356,7 +358,8 @@ class PluginStoreManager:
|
|||||||
# Extract owner/repo from URL
|
# Extract owner/repo from URL
|
||||||
try:
|
try:
|
||||||
# Handle different URL formats
|
# Handle different URL formats
|
||||||
if 'github.com' in repo_url:
|
_parsed_url = urlparse(repo_url)
|
||||||
|
if _parsed_url.hostname in ('github.com', 'www.github.com'):
|
||||||
parts = repo_url.strip('/').split('/')
|
parts = repo_url.strip('/').split('/')
|
||||||
if len(parts) >= 2:
|
if len(parts) >= 2:
|
||||||
owner = parts[-2]
|
owner = parts[-2]
|
||||||
@@ -520,7 +523,8 @@ class PluginStoreManager:
|
|||||||
registry_urls = []
|
registry_urls = []
|
||||||
|
|
||||||
# Extract owner/repo from URL
|
# Extract owner/repo from URL
|
||||||
if 'github.com' in repo_url:
|
_parsed_repo_url = urlparse(repo_url)
|
||||||
|
if _parsed_repo_url.hostname in ('github.com', 'www.github.com'):
|
||||||
parts = repo_url.split('/')
|
parts = repo_url.split('/')
|
||||||
if len(parts) >= 2:
|
if len(parts) >= 2:
|
||||||
owner = parts[-2]
|
owner = parts[-2]
|
||||||
@@ -775,7 +779,8 @@ class PluginStoreManager:
|
|||||||
try:
|
try:
|
||||||
# Convert repo URL to raw content URL
|
# Convert repo URL to raw content URL
|
||||||
# https://github.com/user/repo -> https://raw.githubusercontent.com/user/repo/branch/manifest.json
|
# https://github.com/user/repo -> https://raw.githubusercontent.com/user/repo/branch/manifest.json
|
||||||
if 'github.com' in repo_url:
|
_parsed_manifest_url = urlparse(repo_url)
|
||||||
|
if _parsed_manifest_url.hostname in ('github.com', 'www.github.com'):
|
||||||
# Handle different URL formats
|
# Handle different URL formats
|
||||||
repo_url = repo_url.rstrip('/')
|
repo_url = repo_url.rstrip('/')
|
||||||
if repo_url.endswith('.git'):
|
if repo_url.endswith('.git'):
|
||||||
|
|||||||
@@ -204,20 +204,8 @@ def serve_plugin_asset(plugin_id, filename):
|
|||||||
# Use send_from_directory to serve the file
|
# Use send_from_directory to serve the file
|
||||||
return send_from_directory(str(assets_dir), filename, mimetype=content_type)
|
return send_from_directory(str(assets_dir), filename, mimetype=content_type)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
# Log the exception with full traceback server-side
|
|
||||||
import traceback
|
|
||||||
app.logger.exception('Error serving plugin asset file')
|
app.logger.exception('Error serving plugin asset file')
|
||||||
|
|
||||||
# Return generic error message to client (avoid leaking internal details)
|
|
||||||
# Only include detailed error information when in debug mode
|
|
||||||
if app.debug:
|
|
||||||
return jsonify({
|
|
||||||
'status': 'error',
|
|
||||||
'message': str(e),
|
|
||||||
'traceback': traceback.format_exc()
|
|
||||||
}), 500
|
|
||||||
else:
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'message': 'Internal server error'
|
'message': 'Internal server error'
|
||||||
@@ -342,35 +330,25 @@ def not_found_error(error):
|
|||||||
@app.errorhandler(500)
|
@app.errorhandler(500)
|
||||||
def internal_error(error):
|
def internal_error(error):
|
||||||
"""Handle 500 errors."""
|
"""Handle 500 errors."""
|
||||||
import traceback
|
|
||||||
error_details = traceback.format_exc()
|
|
||||||
|
|
||||||
# Log the error
|
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger('web_interface')
|
logger = logging.getLogger('web_interface')
|
||||||
logger.error(f"Internal server error: {error}", exc_info=True)
|
logger.error("Internal server error", exc_info=True)
|
||||||
|
|
||||||
# Return user-friendly error (hide internal details in production)
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'error_code': 'INTERNAL_ERROR',
|
'error_code': 'INTERNAL_ERROR',
|
||||||
'message': 'An internal error occurred',
|
'message': 'An internal error occurred; see logs for details',
|
||||||
'details': error_details if app.debug else None
|
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
@app.errorhandler(Exception)
|
@app.errorhandler(Exception)
|
||||||
def handle_exception(error):
|
def handle_exception(error):
|
||||||
"""Handle all unhandled exceptions."""
|
"""Handle all unhandled exceptions."""
|
||||||
import traceback
|
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger('web_interface')
|
logger = logging.getLogger('web_interface')
|
||||||
logger.error(f"Unhandled exception: {error}", exc_info=True)
|
logger.error("Unhandled exception", exc_info=True)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'error_code': 'UNKNOWN_ERROR',
|
'error_code': 'UNKNOWN_ERROR',
|
||||||
'message': str(error) if app.debug else 'An error occurred',
|
'message': 'An error occurred; see logs for details',
|
||||||
'details': traceback.format_exc() if app.debug else None
|
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
# Captive portal redirect middleware
|
# Captive portal redirect middleware
|
||||||
@@ -492,7 +470,8 @@ def system_status_generator():
|
|||||||
}
|
}
|
||||||
yield status
|
yield status
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
yield {'error': str(e)}
|
app.logger.error("SSE generator error", exc_info=True)
|
||||||
|
yield {'error': 'An error occurred; see server logs'}
|
||||||
time.sleep(10) # Update every 10 seconds (reduced frequency for better performance)
|
time.sleep(10) # Update every 10 seconds (reduced frequency for better performance)
|
||||||
|
|
||||||
# Display preview generator for SSE
|
# Display preview generator for SSE
|
||||||
@@ -555,7 +534,8 @@ def display_preview_generator():
|
|||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
yield {'error': str(e)}
|
app.logger.error("SSE generator error", exc_info=True)
|
||||||
|
yield {'error': 'An error occurred; see server logs'}
|
||||||
|
|
||||||
time.sleep(1.0) # Check once per second — halves PIL encode overhead vs 0.5s
|
time.sleep(1.0) # Check once per second — halves PIL encode overhead vs 0.5s
|
||||||
|
|
||||||
@@ -598,17 +578,19 @@ def logs_generator():
|
|||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
# Timeout - just skip this update
|
# Timeout - just skip this update
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception:
|
||||||
|
app.logger.error("Error running journalctl", exc_info=True)
|
||||||
error_data = {
|
error_data = {
|
||||||
'timestamp': time.time(),
|
'timestamp': time.time(),
|
||||||
'logs': f'Error running journalctl: {str(e)}'
|
'logs': 'Error running journalctl; see server logs'
|
||||||
}
|
}
|
||||||
yield error_data
|
yield error_data
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
|
app.logger.error("Unexpected error in logs generator", exc_info=True)
|
||||||
error_data = {
|
error_data = {
|
||||||
'timestamp': time.time(),
|
'timestamp': time.time(),
|
||||||
'logs': f'Unexpected error in logs generator: {str(e)}'
|
'logs': 'Unexpected error in logs generator; see server logs'
|
||||||
}
|
}
|
||||||
yield error_data
|
yield error_data
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -84,10 +84,11 @@ def load_partial(partial_name):
|
|||||||
elif partial_name == 'operation-history':
|
elif partial_name == 'operation-history':
|
||||||
return _load_operation_history_partial()
|
return _load_operation_history_partial()
|
||||||
else:
|
else:
|
||||||
return f"Partial '{partial_name}' not found", 404
|
return f"Partial '{escape(partial_name)}' not found", 404
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error loading partial '{partial_name}': {str(e)}", 500
|
logger.error("Error loading partial %s", partial_name, exc_info=True)
|
||||||
|
return "Error loading partial", 500
|
||||||
|
|
||||||
|
|
||||||
@pages_v3.route('/partials/plugin-config/<plugin_id>')
|
@pages_v3.route('/partials/plugin-config/<plugin_id>')
|
||||||
@@ -95,8 +96,9 @@ def load_plugin_config_partial(plugin_id):
|
|||||||
"""Load plugin configuration partial via HTMX - server-side rendered form"""
|
"""Load plugin configuration partial via HTMX - server-side rendered form"""
|
||||||
try:
|
try:
|
||||||
return _load_plugin_config_partial(plugin_id)
|
return _load_plugin_config_partial(plugin_id)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
return f'<div class="text-red-500 p-4">Error loading plugin config: {escape(str(e))}</div>', 500
|
logger.error("Error loading plugin config partial for %s", plugin_id, exc_info=True)
|
||||||
|
return '<div class="text-red-500 p-4">Error loading plugin config; see logs for details</div>', 500
|
||||||
|
|
||||||
def _load_overview_partial():
|
def _load_overview_partial():
|
||||||
"""Load overview partial with system stats"""
|
"""Load overview partial with system stats"""
|
||||||
@@ -107,7 +109,8 @@ def _load_overview_partial():
|
|||||||
return render_template('v3/partials/overview.html',
|
return render_template('v3/partials/overview.html',
|
||||||
main_config=main_config)
|
main_config=main_config)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error: {str(e)}", 500
|
logger.error("Error loading partial", exc_info=True)
|
||||||
|
return "Error loading partial", 500
|
||||||
|
|
||||||
def _load_general_partial():
|
def _load_general_partial():
|
||||||
"""Load general settings partial"""
|
"""Load general settings partial"""
|
||||||
@@ -117,7 +120,8 @@ def _load_general_partial():
|
|||||||
return render_template('v3/partials/general.html',
|
return render_template('v3/partials/general.html',
|
||||||
main_config=main_config)
|
main_config=main_config)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error: {str(e)}", 500
|
logger.error("Error loading partial", exc_info=True)
|
||||||
|
return "Error loading partial", 500
|
||||||
|
|
||||||
def _load_display_partial():
|
def _load_display_partial():
|
||||||
"""Load display settings partial"""
|
"""Load display settings partial"""
|
||||||
@@ -127,7 +131,8 @@ def _load_display_partial():
|
|||||||
return render_template('v3/partials/display.html',
|
return render_template('v3/partials/display.html',
|
||||||
main_config=main_config)
|
main_config=main_config)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error: {str(e)}", 500
|
logger.error("Error loading partial", exc_info=True)
|
||||||
|
return "Error loading partial", 500
|
||||||
|
|
||||||
def _load_durations_partial():
|
def _load_durations_partial():
|
||||||
"""Load display durations partial"""
|
"""Load display durations partial"""
|
||||||
@@ -137,7 +142,8 @@ def _load_durations_partial():
|
|||||||
return render_template('v3/partials/durations.html',
|
return render_template('v3/partials/durations.html',
|
||||||
main_config=main_config)
|
main_config=main_config)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error: {str(e)}", 500
|
logger.error("Error loading partial", exc_info=True)
|
||||||
|
return "Error loading partial", 500
|
||||||
|
|
||||||
def _load_schedule_partial():
|
def _load_schedule_partial():
|
||||||
"""Load schedule settings partial"""
|
"""Load schedule settings partial"""
|
||||||
@@ -153,7 +159,8 @@ def _load_schedule_partial():
|
|||||||
dim_schedule_config=dim_schedule_config,
|
dim_schedule_config=dim_schedule_config,
|
||||||
normal_brightness=normal_brightness)
|
normal_brightness=normal_brightness)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error: {str(e)}", 500
|
logger.error("Error loading partial", exc_info=True)
|
||||||
|
return "Error loading partial", 500
|
||||||
|
|
||||||
|
|
||||||
def _load_weather_partial():
|
def _load_weather_partial():
|
||||||
@@ -164,7 +171,8 @@ def _load_weather_partial():
|
|||||||
return render_template('v3/partials/weather.html',
|
return render_template('v3/partials/weather.html',
|
||||||
main_config=main_config)
|
main_config=main_config)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error: {str(e)}", 500
|
logger.error("Error loading partial", exc_info=True)
|
||||||
|
return "Error loading partial", 500
|
||||||
|
|
||||||
def _load_stocks_partial():
|
def _load_stocks_partial():
|
||||||
"""Load stocks configuration partial"""
|
"""Load stocks configuration partial"""
|
||||||
@@ -174,7 +182,8 @@ def _load_stocks_partial():
|
|||||||
return render_template('v3/partials/stocks.html',
|
return render_template('v3/partials/stocks.html',
|
||||||
main_config=main_config)
|
main_config=main_config)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error: {str(e)}", 500
|
logger.error("Error loading partial", exc_info=True)
|
||||||
|
return "Error loading partial", 500
|
||||||
|
|
||||||
def _load_plugins_partial():
|
def _load_plugins_partial():
|
||||||
"""Load plugins management partial"""
|
"""Load plugins management partial"""
|
||||||
@@ -208,7 +217,7 @@ def _load_plugins_partial():
|
|||||||
plugin_info.update(fresh_manifest)
|
plugin_info.update(fresh_manifest)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# If we can't read the fresh manifest, use the cached one
|
# If we can't read the fresh manifest, use the cached one
|
||||||
print(f"Warning: Could not read fresh manifest for {plugin_id}: {e}")
|
logger.warning("Could not read fresh manifest for {plugin_id}")
|
||||||
|
|
||||||
# Get enabled status from config (source of truth)
|
# Get enabled status from config (source of truth)
|
||||||
# Read from config file first, fall back to plugin instance if config doesn't have the key
|
# Read from config file first, fall back to plugin instance if config doesn't have the key
|
||||||
@@ -256,12 +265,13 @@ def _load_plugins_partial():
|
|||||||
'branch': branch
|
'branch': branch
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error loading plugin data: {e}")
|
logger.error("Error loading plugin data", exc_info=True)
|
||||||
|
|
||||||
return render_template('v3/partials/plugins.html',
|
return render_template('v3/partials/plugins.html',
|
||||||
plugins=plugins_data)
|
plugins=plugins_data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error: {str(e)}", 500
|
logger.error("Error loading partial", exc_info=True)
|
||||||
|
return "Error loading partial", 500
|
||||||
|
|
||||||
def _load_fonts_partial():
|
def _load_fonts_partial():
|
||||||
"""Load fonts management partial"""
|
"""Load fonts management partial"""
|
||||||
@@ -271,14 +281,16 @@ def _load_fonts_partial():
|
|||||||
return render_template('v3/partials/fonts.html',
|
return render_template('v3/partials/fonts.html',
|
||||||
fonts=fonts_data)
|
fonts=fonts_data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error: {str(e)}", 500
|
logger.error("Error loading partial", exc_info=True)
|
||||||
|
return "Error loading partial", 500
|
||||||
|
|
||||||
def _load_logs_partial():
|
def _load_logs_partial():
|
||||||
"""Load logs viewer partial"""
|
"""Load logs viewer partial"""
|
||||||
try:
|
try:
|
||||||
return render_template('v3/partials/logs.html')
|
return render_template('v3/partials/logs.html')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error: {str(e)}", 500
|
logger.error("Error loading partial", exc_info=True)
|
||||||
|
return "Error loading partial", 500
|
||||||
|
|
||||||
def _load_raw_json_partial():
|
def _load_raw_json_partial():
|
||||||
"""Load raw JSON editor partial"""
|
"""Load raw JSON editor partial"""
|
||||||
@@ -295,14 +307,16 @@ def _load_raw_json_partial():
|
|||||||
main_config_path=pages_v3.config_manager.get_config_path(),
|
main_config_path=pages_v3.config_manager.get_config_path(),
|
||||||
secrets_config_path=pages_v3.config_manager.get_secrets_path())
|
secrets_config_path=pages_v3.config_manager.get_secrets_path())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error: {str(e)}", 500
|
logger.error("Error loading partial", exc_info=True)
|
||||||
|
return "Error loading partial", 500
|
||||||
|
|
||||||
def _load_backup_restore_partial():
|
def _load_backup_restore_partial():
|
||||||
"""Load backup & restore partial."""
|
"""Load backup & restore partial."""
|
||||||
try:
|
try:
|
||||||
return render_template('v3/partials/backup_restore.html')
|
return render_template('v3/partials/backup_restore.html')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error: {str(e)}", 500
|
logger.error("Error loading partial", exc_info=True)
|
||||||
|
return "Error loading partial", 500
|
||||||
|
|
||||||
@pages_v3.route('/setup')
|
@pages_v3.route('/setup')
|
||||||
def captive_setup():
|
def captive_setup():
|
||||||
@@ -314,21 +328,24 @@ def _load_wifi_partial():
|
|||||||
try:
|
try:
|
||||||
return render_template('v3/partials/wifi.html')
|
return render_template('v3/partials/wifi.html')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error: {str(e)}", 500
|
logger.error("Error loading partial", exc_info=True)
|
||||||
|
return "Error loading partial", 500
|
||||||
|
|
||||||
def _load_cache_partial():
|
def _load_cache_partial():
|
||||||
"""Load cache management partial"""
|
"""Load cache management partial"""
|
||||||
try:
|
try:
|
||||||
return render_template('v3/partials/cache.html')
|
return render_template('v3/partials/cache.html')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error: {str(e)}", 500
|
logger.error("Error loading partial", exc_info=True)
|
||||||
|
return "Error loading partial", 500
|
||||||
|
|
||||||
def _load_operation_history_partial():
|
def _load_operation_history_partial():
|
||||||
"""Load operation history partial"""
|
"""Load operation history partial"""
|
||||||
try:
|
try:
|
||||||
return render_template('v3/partials/operation_history.html')
|
return render_template('v3/partials/operation_history.html')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error: {str(e)}", 500
|
logger.error("Error loading partial", exc_info=True)
|
||||||
|
return "Error loading partial", 500
|
||||||
|
|
||||||
|
|
||||||
def _load_plugin_config_partial(plugin_id):
|
def _load_plugin_config_partial(plugin_id):
|
||||||
@@ -336,6 +353,11 @@ def _load_plugin_config_partial(plugin_id):
|
|||||||
Load plugin configuration partial - server-side rendered form.
|
Load plugin configuration partial - server-side rendered form.
|
||||||
This replaces the client-side generateConfigForm() JavaScript.
|
This replaces the client-side generateConfigForm() JavaScript.
|
||||||
"""
|
"""
|
||||||
|
import re as _re
|
||||||
|
# Reject plugin IDs containing path-traversal characters before any filesystem use
|
||||||
|
if not _re.match(r'^[a-zA-Z0-9_\-.:]+$', plugin_id or ''):
|
||||||
|
return '<div class="text-red-500 p-4">Invalid plugin ID</div>', 400
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not pages_v3.plugin_manager:
|
if not pages_v3.plugin_manager:
|
||||||
return '<div class="text-red-500 p-4">Plugin manager not available</div>', 500
|
return '<div class="text-red-500 p-4">Plugin manager not available</div>', 500
|
||||||
@@ -394,7 +416,7 @@ def _load_plugin_config_partial(plugin_id):
|
|||||||
if new_images:
|
if new_images:
|
||||||
config['images'] = config.get('images', []) + new_images
|
config['images'] = config.get('images', []) + new_images
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Warning: Could not load metadata for {plugin_id}: {e}")
|
logger.warning("Could not load metadata for {plugin_id}")
|
||||||
except Exception as e: # nosec B110 - metadata pre-load is optional; schema loads fully below
|
except Exception as e: # nosec B110 - metadata pre-load is optional; schema loads fully below
|
||||||
logger.debug("Metadata pre-load skipped for plugin %s: %s", plugin_id, e)
|
logger.debug("Metadata pre-load skipped for plugin %s: %s", plugin_id, e)
|
||||||
|
|
||||||
@@ -406,7 +428,7 @@ def _load_plugin_config_partial(plugin_id):
|
|||||||
with open(schema_path, 'r', encoding='utf-8') as f:
|
with open(schema_path, 'r', encoding='utf-8') as f:
|
||||||
schema = json.load(f)
|
schema = json.load(f)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Warning: Could not load schema for {plugin_id}: {e}")
|
logger.warning("Could not load schema for {plugin_id}")
|
||||||
|
|
||||||
# Get web UI actions from plugin manifest
|
# Get web UI actions from plugin manifest
|
||||||
web_ui_actions = []
|
web_ui_actions = []
|
||||||
@@ -417,7 +439,7 @@ def _load_plugin_config_partial(plugin_id):
|
|||||||
manifest = json.load(f)
|
manifest = json.load(f)
|
||||||
web_ui_actions = manifest.get('web_ui_actions', [])
|
web_ui_actions = manifest.get('web_ui_actions', [])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Warning: Could not load manifest for {plugin_id}: {e}")
|
logger.warning("Could not load manifest for {plugin_id}")
|
||||||
|
|
||||||
# Mask secret fields before rendering template (fail closed — never leak secrets)
|
# Mask secret fields before rendering template (fail closed — never leak secrets)
|
||||||
schema_properties = schema.get('properties') if isinstance(schema, dict) else None
|
schema_properties = schema.get('properties') if isinstance(schema, dict) else None
|
||||||
@@ -453,9 +475,8 @@ def _load_plugin_config_partial(plugin_id):
|
|||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
logger.error("Error loading plugin config partial for %s", plugin_id, exc_info=True)
|
||||||
traceback.print_exc()
|
return '<div class="text-red-500 p-4">Error loading plugin config; see logs for details</div>', 500
|
||||||
return f'<div class="text-red-500 p-4">Error loading plugin config: {escape(str(e))}</div>', 500
|
|
||||||
|
|
||||||
|
|
||||||
def _load_starlark_config_partial(app_id):
|
def _load_starlark_config_partial(app_id):
|
||||||
|
|||||||
@@ -51,8 +51,10 @@
|
|||||||
sanitizeValue(value) {
|
sanitizeValue(value) {
|
||||||
// Base implementation - widgets should override for specific needs
|
// Base implementation - widgets should override for specific needs
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
// Basic XSS prevention
|
// Strip all HTML tags via the DOM parser to prevent XSS
|
||||||
return value.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
|
const div = document.createElement('div');
|
||||||
|
div.textContent = value;
|
||||||
|
return div.textContent;
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1442,9 +1442,14 @@ function renderInstalledPlugins(plugins) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to escape attributes for use in HTML
|
// Helper function to escape values for use in HTML attributes
|
||||||
const escapeAttr = (text) => {
|
const escapeAttr = (text) => {
|
||||||
return (text || '').replace(/'/g, "\\'").replace(/"/g, '"');
|
return (text || '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to escape for JavaScript strings (use JSON.stringify for proper escaping)
|
// Helper function to escape for JavaScript strings (use JSON.stringify for proper escaping)
|
||||||
@@ -4507,6 +4512,8 @@ function syncFormToJson() {
|
|||||||
// Deep merge with existing config to preserve nested structures
|
// Deep merge with existing config to preserve nested structures
|
||||||
function deepMerge(target, source) {
|
function deepMerge(target, source) {
|
||||||
for (const key in source) {
|
for (const key in source) {
|
||||||
|
if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(source, key)) continue;
|
||||||
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
||||||
if (!target[key] || typeof target[key] !== 'object' || Array.isArray(target[key])) {
|
if (!target[key] || typeof target[key] !== 'object' || Array.isArray(target[key])) {
|
||||||
target[key] = {};
|
target[key] = {};
|
||||||
@@ -7473,17 +7480,28 @@ setTimeout(function() {
|
|||||||
console.log('installed-plugins-grid not found yet, will retry via event listeners');
|
console.log('installed-plugins-grid not found yet, will retry via event listeners');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also try to attach install button handler after a delay (fallback)
|
// Also try to attach install button handler after a delay (fallback).
|
||||||
|
// Only run if the install button element is already in the DOM (i.e. the
|
||||||
|
// plugins partial has been loaded); otherwise the htmx:afterSettle listener
|
||||||
|
// below handles it when the tab is first visited.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (typeof window.attachInstallButtonHandler === 'function') {
|
if (typeof window.attachInstallButtonHandler === 'function' &&
|
||||||
console.log('[FALLBACK] Attempting to attach install button handler...');
|
document.getElementById('install-plugin-from-url')) {
|
||||||
window.attachInstallButtonHandler();
|
window.attachInstallButtonHandler();
|
||||||
} else {
|
|
||||||
console.warn('[FALLBACK] attachInstallButtonHandler not available on window');
|
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|
||||||
|
// Re-run install button wiring after HTMX settles the plugins tab content.
|
||||||
|
// Guard with element check so it only fires when the plugins partial is in the DOM,
|
||||||
|
// preventing spurious warnings on other tab loads.
|
||||||
|
document.addEventListener('htmx:afterSettle', function() {
|
||||||
|
if (document.getElementById('install-plugin-from-url') &&
|
||||||
|
typeof window.attachInstallButtonHandler === 'function') {
|
||||||
|
window.attachInstallButtonHandler();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ─── Starlark Apps Integration ──────────────────────────────────────────────
|
// ─── Starlark Apps Integration ──────────────────────────────────────────────
|
||||||
|
|
||||||
(function() {
|
(function() {
|
||||||
|
|||||||
@@ -136,6 +136,7 @@
|
|||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
if (typeof htmx !== 'undefined') {
|
if (typeof htmx !== 'undefined') {
|
||||||
console.log('HTMX loaded from fallback');
|
console.log('HTMX loaded from fallback');
|
||||||
|
window.dispatchEvent(new Event('htmx:ready'));
|
||||||
// Load extensions after core loads
|
// Load extensions after core loads
|
||||||
loadScript(sseSrc, isAPMode ? 'https://unpkg.com/htmx.org/dist/ext/sse.js' : '/static/v3/js/htmx-sse.js');
|
loadScript(sseSrc, isAPMode ? 'https://unpkg.com/htmx.org/dist/ext/sse.js' : '/static/v3/js/htmx-sse.js');
|
||||||
loadScript(jsonEncSrc, isAPMode ? 'https://unpkg.com/htmx.org/dist/ext/json-enc.js' : '/static/v3/js/htmx-json-enc.js');
|
loadScript(jsonEncSrc, isAPMode ? 'https://unpkg.com/htmx.org/dist/ext/json-enc.js' : '/static/v3/js/htmx-json-enc.js');
|
||||||
@@ -152,6 +153,7 @@
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('HTMX loaded successfully');
|
console.log('HTMX loaded successfully');
|
||||||
|
window.dispatchEvent(new Event('htmx:ready'));
|
||||||
// Load extensions after core loads
|
// Load extensions after core loads
|
||||||
loadScript(sseSrc, isAPMode ? 'https://unpkg.com/htmx.org/dist/ext/sse.js' : '/static/v3/js/htmx-sse.js');
|
loadScript(sseSrc, isAPMode ? 'https://unpkg.com/htmx.org/dist/ext/sse.js' : '/static/v3/js/htmx-sse.js');
|
||||||
loadScript(jsonEncSrc, isAPMode ? 'https://unpkg.com/htmx.org/dist/ext/json-enc.js' : '/static/v3/js/htmx-json-enc.js');
|
loadScript(jsonEncSrc, isAPMode ? 'https://unpkg.com/htmx.org/dist/ext/json-enc.js' : '/static/v3/js/htmx-json-enc.js');
|
||||||
@@ -349,6 +351,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set data-loaded on tab containers after HTMX settles their content,
|
||||||
|
// preventing repeated re-fetches on every tab switch.
|
||||||
|
// Scoped to elements with hx-trigger="revealed" (tab containers only) so
|
||||||
|
// modals and plugin config panels that legitimately reload are unaffected.
|
||||||
|
document.body.addEventListener('htmx:afterSettle', function(event) {
|
||||||
|
if (event.detail && event.detail.target) {
|
||||||
|
var target = event.detail.target;
|
||||||
|
var trigger = target.getAttribute('hx-trigger') || '';
|
||||||
|
if (trigger.includes('revealed')) {
|
||||||
|
target.setAttribute('data-loaded', 'true');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', setupScriptExecution);
|
document.addEventListener('DOMContentLoaded', setupScriptExecution);
|
||||||
@@ -411,6 +427,9 @@
|
|||||||
.then(html => {
|
.then(html => {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
content.innerHTML = html;
|
content.innerHTML = html;
|
||||||
|
if (typeof htmx !== 'undefined') {
|
||||||
|
htmx.process(content);
|
||||||
|
}
|
||||||
// Trigger full initialization chain
|
// Trigger full initialization chain
|
||||||
if (window.pluginManager) {
|
if (window.pluginManager) {
|
||||||
window.pluginManager.initialized = false;
|
window.pluginManager.initialized = false;
|
||||||
@@ -430,7 +449,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fallback if HTMX doesn't load within 5 seconds
|
// Fallback if HTMX doesn't load within 5 seconds
|
||||||
setTimeout(() => {
|
var _pluginsFallbackTimer = setTimeout(() => {
|
||||||
if (typeof htmx === 'undefined') {
|
if (typeof htmx === 'undefined') {
|
||||||
console.warn('HTMX not loaded after 5 seconds, using direct fetch for plugins');
|
console.warn('HTMX not loaded after 5 seconds, using direct fetch for plugins');
|
||||||
// Load plugins tab content directly regardless of active tab,
|
// Load plugins tab content directly regardless of active tab,
|
||||||
@@ -438,6 +457,7 @@
|
|||||||
loadPluginsDirect();
|
loadPluginsDirect();
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
window.addEventListener('htmx:ready', function() { clearTimeout(_pluginsFallbackTimer); }, { once: true });
|
||||||
</script>
|
</script>
|
||||||
<!-- Alpine.js app function - defined early so it's available when Alpine initializes -->
|
<!-- Alpine.js app function - defined early so it's available when Alpine initializes -->
|
||||||
<script>
|
<script>
|
||||||
@@ -1030,6 +1050,9 @@
|
|||||||
.then(html => {
|
.then(html => {
|
||||||
overviewContent.innerHTML = html;
|
overviewContent.innerHTML = html;
|
||||||
overviewContent.setAttribute('data-loaded', 'true');
|
overviewContent.setAttribute('data-loaded', 'true');
|
||||||
|
if (typeof htmx !== 'undefined') {
|
||||||
|
htmx.process(overviewContent);
|
||||||
|
}
|
||||||
// Re-initialize Alpine.js for the new content
|
// Re-initialize Alpine.js for the new content
|
||||||
if (window.Alpine) {
|
if (window.Alpine) {
|
||||||
window.Alpine.initTree(overviewContent);
|
window.Alpine.initTree(overviewContent);
|
||||||
@@ -1058,7 +1081,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Also try direct load if HTMX doesn't load within 5 seconds
|
// Also try direct load if HTMX doesn't load within 5 seconds
|
||||||
setTimeout(() => {
|
var _overviewFallbackTimer = setTimeout(() => {
|
||||||
if (typeof htmx === 'undefined') {
|
if (typeof htmx === 'undefined') {
|
||||||
console.warn('HTMX not loaded after 5 seconds, using direct fetch for content');
|
console.warn('HTMX not loaded after 5 seconds, using direct fetch for content');
|
||||||
const appElement = document.querySelector('[x-data="app()"]');
|
const appElement = document.querySelector('[x-data="app()"]');
|
||||||
@@ -1070,6 +1093,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
window.addEventListener('htmx:ready', function() { clearTimeout(_overviewFallbackTimer); }, { once: true });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- General tab -->
|
<!-- General tab -->
|
||||||
@@ -1816,13 +1840,18 @@
|
|||||||
htmx.trigger(contentEl, 'revealed');
|
htmx.trigger(contentEl, 'revealed');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// HTMX not available, use direct fetch
|
// HTMX is still loading asynchronously — retry when it signals ready,
|
||||||
console.warn('HTMX not available, using direct fetch for tab:', tab);
|
// or fall back to direct fetch if it fails to load entirely.
|
||||||
if (tab === 'overview' && typeof loadOverviewDirect === 'function') {
|
const self = this;
|
||||||
loadOverviewDirect();
|
function onReady() { window.removeEventListener('htmx-load-failed', onFailed); self.loadTabContent(tab); }
|
||||||
} else if (tab === 'wifi' && typeof loadWifiDirect === 'function') {
|
function onFailed() {
|
||||||
loadWifiDirect();
|
window.removeEventListener('htmx:ready', onReady);
|
||||||
|
if (tab === 'overview' && typeof loadOverviewDirect === 'function') loadOverviewDirect();
|
||||||
|
else if (tab === 'wifi' && typeof loadWifiDirect === 'function') loadWifiDirect();
|
||||||
|
else if (tab === 'plugins' && typeof loadPluginsDirect === 'function') loadPluginsDirect();
|
||||||
}
|
}
|
||||||
|
window.addEventListener('htmx:ready', onReady, { once: true });
|
||||||
|
window.addEventListener('htmx-load-failed', onFailed, { once: true });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
<button hx-post="/api/v3/system/action"
|
<button hx-post="/api/v3/system/action"
|
||||||
hx-vals='{"action": "start_display"}'
|
hx-vals='{"action": "start_display"}'
|
||||||
hx-swap="none"
|
hx-swap="none"
|
||||||
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined' && event.detail.xhr && event.detail.xhr.responseJSON) { showNotification(event.detail.xhr.responseJSON.message || 'Display started', event.detail.xhr.responseJSON.status || 'success'); }"
|
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined') { var m='Display started',s='success'; try { var d=JSON.parse(event.detail.xhr.responseText); m=d.message||m; s=d.status||s; } catch(e) { s=(event.detail.xhr&&event.detail.xhr.status>=400?'error':s); } showNotification(m,s); }"
|
||||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700">
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700">
|
||||||
<i class="fas fa-play mr-2"></i>
|
<i class="fas fa-play mr-2"></i>
|
||||||
Start Display
|
Start Display
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
<button hx-post="/api/v3/system/action"
|
<button hx-post="/api/v3/system/action"
|
||||||
hx-vals='{"action": "stop_display"}'
|
hx-vals='{"action": "stop_display"}'
|
||||||
hx-swap="none"
|
hx-swap="none"
|
||||||
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined' && event.detail.xhr && event.detail.xhr.responseJSON) { showNotification(event.detail.xhr.responseJSON.message || 'Display stopped', event.detail.xhr.responseJSON.status || 'success'); }"
|
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined') { var m='Display stopped',s='success'; try { var d=JSON.parse(event.detail.xhr.responseText); m=d.message||m; s=d.status||s; } catch(e) { s=(event.detail.xhr&&event.detail.xhr.status>=400?'error':s); } showNotification(m,s); }"
|
||||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700">
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700">
|
||||||
<i class="fas fa-stop mr-2"></i>
|
<i class="fas fa-stop mr-2"></i>
|
||||||
Stop Display
|
Stop Display
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
<button hx-post="/api/v3/system/action"
|
<button hx-post="/api/v3/system/action"
|
||||||
hx-vals='{"action": "git_pull"}'
|
hx-vals='{"action": "git_pull"}'
|
||||||
hx-swap="none"
|
hx-swap="none"
|
||||||
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined' && event.detail.xhr && event.detail.xhr.responseJSON) { showNotification(event.detail.xhr.responseJSON.message || 'Code update completed', event.detail.xhr.responseJSON.status || 'info'); }"
|
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined') { var m='Code update completed',s='info'; try { var d=JSON.parse(event.detail.xhr.responseText); m=d.message||m; s=d.status||s; } catch(e) { s=(event.detail.xhr&&event.detail.xhr.status>=400?'error':s); } showNotification(m,s); }"
|
||||||
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||||
<i class="fas fa-download mr-2"></i>
|
<i class="fas fa-download mr-2"></i>
|
||||||
Update Code
|
Update Code
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
hx-vals='{"action": "reboot_system"}'
|
hx-vals='{"action": "reboot_system"}'
|
||||||
hx-confirm="Are you sure you want to reboot the system?"
|
hx-confirm="Are you sure you want to reboot the system?"
|
||||||
hx-swap="none"
|
hx-swap="none"
|
||||||
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined' && event.detail.xhr && event.detail.xhr.responseJSON) { showNotification(event.detail.xhr.responseJSON.message || 'System rebooting...', event.detail.xhr.responseJSON.status || 'info'); }"
|
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined') { var m='System rebooting...',s='info'; try { var d=JSON.parse(event.detail.xhr.responseText); m=d.message||m; s=d.status||s; } catch(e) { s=(event.detail.xhr&&event.detail.xhr.status>=400?'error':s); } showNotification(m,s); }"
|
||||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-yellow-600 hover:bg-yellow-700">
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-yellow-600 hover:bg-yellow-700">
|
||||||
<i class="fas fa-power-off mr-2"></i>
|
<i class="fas fa-power-off mr-2"></i>
|
||||||
Reboot System
|
Reboot System
|
||||||
|
|||||||
@@ -151,7 +151,7 @@
|
|||||||
<button hx-post="/api/v3/system/action"
|
<button hx-post="/api/v3/system/action"
|
||||||
hx-vals='{"action": "start_display"}'
|
hx-vals='{"action": "start_display"}'
|
||||||
hx-swap="none"
|
hx-swap="none"
|
||||||
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined' && event.detail.xhr && event.detail.xhr.responseJSON) { showNotification(event.detail.xhr.responseJSON.message || 'Display started', event.detail.xhr.responseJSON.status || 'success'); }"
|
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined') { var m='Display started',s='success'; try { var d=JSON.parse(event.detail.xhr.responseText); m=d.message||m; s=d.status||s; } catch(e) { s=(event.detail.xhr&&event.detail.xhr.status>=400?'error':s); } showNotification(m,s); }"
|
||||||
class="inline-flex items-center px-4 py-2 border border-transparent text-base font-semibold rounded-md text-white bg-green-600 hover:bg-green-700">
|
class="inline-flex items-center px-4 py-2 border border-transparent text-base font-semibold rounded-md text-white bg-green-600 hover:bg-green-700">
|
||||||
<i class="fas fa-play mr-2"></i>
|
<i class="fas fa-play mr-2"></i>
|
||||||
Start Display
|
Start Display
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
<button hx-post="/api/v3/system/action"
|
<button hx-post="/api/v3/system/action"
|
||||||
hx-vals='{"action": "stop_display"}'
|
hx-vals='{"action": "stop_display"}'
|
||||||
hx-swap="none"
|
hx-swap="none"
|
||||||
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined' && event.detail.xhr && event.detail.xhr.responseJSON) { showNotification(event.detail.xhr.responseJSON.message || 'Display stopped', event.detail.xhr.responseJSON.status || 'success'); }"
|
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined') { var m='Display stopped',s='success'; try { var d=JSON.parse(event.detail.xhr.responseText); m=d.message||m; s=d.status||s; } catch(e) { s=(event.detail.xhr&&event.detail.xhr.status>=400?'error':s); } showNotification(m,s); }"
|
||||||
class="inline-flex items-center px-4 py-2 border border-transparent text-base font-semibold rounded-md text-white bg-red-600 hover:bg-red-700">
|
class="inline-flex items-center px-4 py-2 border border-transparent text-base font-semibold rounded-md text-white bg-red-600 hover:bg-red-700">
|
||||||
<i class="fas fa-stop mr-2"></i>
|
<i class="fas fa-stop mr-2"></i>
|
||||||
Stop Display
|
Stop Display
|
||||||
@@ -170,7 +170,7 @@
|
|||||||
hx-vals='{"action": "git_pull"}'
|
hx-vals='{"action": "git_pull"}'
|
||||||
hx-confirm="This will stash any local changes and update the code. Continue?"
|
hx-confirm="This will stash any local changes and update the code. Continue?"
|
||||||
hx-swap="none"
|
hx-swap="none"
|
||||||
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined' && event.detail.xhr && event.detail.xhr.responseJSON) { showNotification(event.detail.xhr.responseJSON.message || 'Code update completed', event.detail.xhr.responseJSON.status || 'info'); }"
|
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined') { var m='Code update completed',s='info'; try { var d=JSON.parse(event.detail.xhr.responseText); m=d.message||m; s=d.status||s; } catch(e) { s=(event.detail.xhr&&event.detail.xhr.status>=400?'error':s); } showNotification(m,s); }"
|
||||||
class="inline-flex items-center px-4 py-2 border border-gray-300 text-base font-semibold rounded-md text-gray-900 bg-white hover:bg-gray-50">
|
class="inline-flex items-center px-4 py-2 border border-gray-300 text-base font-semibold rounded-md text-gray-900 bg-white hover:bg-gray-50">
|
||||||
<i class="fas fa-download mr-2"></i>
|
<i class="fas fa-download mr-2"></i>
|
||||||
Update Code
|
Update Code
|
||||||
@@ -180,7 +180,7 @@
|
|||||||
hx-vals='{"action": "reboot_system"}'
|
hx-vals='{"action": "reboot_system"}'
|
||||||
hx-confirm="Are you sure you want to reboot the system?"
|
hx-confirm="Are you sure you want to reboot the system?"
|
||||||
hx-swap="none"
|
hx-swap="none"
|
||||||
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined' && event.detail.xhr && event.detail.xhr.responseJSON) { showNotification(event.detail.xhr.responseJSON.message || 'System rebooting...', event.detail.xhr.responseJSON.status || 'info'); }"
|
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined') { var m='System rebooting...',s='info'; try { var d=JSON.parse(event.detail.xhr.responseText); m=d.message||m; s=d.status||s; } catch(e) { s=(event.detail.xhr&&event.detail.xhr.status>=400?'error':s); } showNotification(m,s); }"
|
||||||
class="inline-flex items-center px-4 py-2 border border-transparent text-base font-semibold rounded-md text-white bg-yellow-600 hover:bg-yellow-700">
|
class="inline-flex items-center px-4 py-2 border border-transparent text-base font-semibold rounded-md text-white bg-yellow-600 hover:bg-yellow-700">
|
||||||
<i class="fas fa-power-off mr-2"></i>
|
<i class="fas fa-power-off mr-2"></i>
|
||||||
Reboot System
|
Reboot System
|
||||||
@@ -190,7 +190,7 @@
|
|||||||
hx-vals='{"action": "shutdown_system"}'
|
hx-vals='{"action": "shutdown_system"}'
|
||||||
hx-confirm="Are you sure you want to shut down the system? This will power off the Raspberry Pi."
|
hx-confirm="Are you sure you want to shut down the system? This will power off the Raspberry Pi."
|
||||||
hx-swap="none"
|
hx-swap="none"
|
||||||
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined' && event.detail.xhr && event.detail.xhr.responseJSON) { showNotification(event.detail.xhr.responseJSON.message || 'System shutting down...', event.detail.xhr.responseJSON.status || 'info'); }"
|
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined') { var m='System shutting down...',s='info'; try { var d=JSON.parse(event.detail.xhr.responseText); m=d.message||m; s=d.status||s; } catch(e) { s=(event.detail.xhr&&event.detail.xhr.status>=400?'error':s); } showNotification(m,s); }"
|
||||||
class="inline-flex items-center px-4 py-2 border border-transparent text-base font-semibold rounded-md text-white bg-red-800 hover:bg-red-900">
|
class="inline-flex items-center px-4 py-2 border border-transparent text-base font-semibold rounded-md text-white bg-red-800 hover:bg-red-900">
|
||||||
<i class="fas fa-power-off mr-2"></i>
|
<i class="fas fa-power-off mr-2"></i>
|
||||||
Shutdown System
|
Shutdown System
|
||||||
@@ -199,7 +199,7 @@
|
|||||||
<button hx-post="/api/v3/system/action"
|
<button hx-post="/api/v3/system/action"
|
||||||
hx-vals='{"action": "restart_display_service"}'
|
hx-vals='{"action": "restart_display_service"}'
|
||||||
hx-swap="none"
|
hx-swap="none"
|
||||||
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined' && event.detail.xhr && event.detail.xhr.responseJSON) { showNotification(event.detail.xhr.responseJSON.message || 'Display service restarted', event.detail.xhr.responseJSON.status || 'success'); }"
|
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined') { var m='Display service restarted',s='success'; try { var d=JSON.parse(event.detail.xhr.responseText); m=d.message||m; s=d.status||s; } catch(e) { s=(event.detail.xhr&&event.detail.xhr.status>=400?'error':s); } showNotification(m,s); }"
|
||||||
class="inline-flex items-center px-4 py-2 border border-gray-300 text-base font-semibold rounded-md text-gray-900 bg-white hover:bg-gray-50">
|
class="inline-flex items-center px-4 py-2 border border-gray-300 text-base font-semibold rounded-md text-gray-900 bg-white hover:bg-gray-50">
|
||||||
<i class="fas fa-redo mr-2"></i>
|
<i class="fas fa-redo mr-2"></i>
|
||||||
Restart Display Service
|
Restart Display Service
|
||||||
@@ -208,7 +208,7 @@
|
|||||||
<button hx-post="/api/v3/system/action"
|
<button hx-post="/api/v3/system/action"
|
||||||
hx-vals='{"action": "restart_web_service"}'
|
hx-vals='{"action": "restart_web_service"}'
|
||||||
hx-swap="none"
|
hx-swap="none"
|
||||||
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined' && event.detail.xhr && event.detail.xhr.responseJSON) { showNotification(event.detail.xhr.responseJSON.message || 'Web service restarted', event.detail.xhr.responseJSON.status || 'success'); }"
|
hx-on:htmx:after-request="if (typeof showNotification !== 'undefined') { var m='Web service restarted',s='success'; try { var d=JSON.parse(event.detail.xhr.responseText); m=d.message||m; s=d.status||s; } catch(e) { s=(event.detail.xhr&&event.detail.xhr.status>=400?'error':s); } showNotification(m,s); }"
|
||||||
class="inline-flex items-center px-4 py-2 border border-gray-300 text-base font-semibold rounded-md text-gray-900 bg-white hover:bg-gray-50">
|
class="inline-flex items-center px-4 py-2 border border-gray-300 text-base font-semibold rounded-md text-gray-900 bg-white hover:bg-gray-50">
|
||||||
<i class="fas fa-redo mr-2"></i>
|
<i class="fas fa-redo mr-2"></i>
|
||||||
Restart Web Service
|
Restart Web Service
|
||||||
|
|||||||
Reference in New Issue
Block a user