mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-05-25 13:43:31 +00:00
* fix(web-ui): fix quick actions not firing, add toast feedback, suppress install handler warning - base.html: add htmx:afterSettle listener to set data-loaded on tab containers after HTMX swaps their content, preventing the overview partial from being re-fetched (and handlers lost) on every tab switch - base.html: call htmx.process() in loadOverviewDirect/loadPluginsDirect fallbacks so buttons get HTMX handlers even if HTMX finished its initial body scan before the fallback fetch completed - overview.html + index.html (11 buttons): replace event.detail.xhr.responseJSON (undefined in HTMX 1.9.x) with JSON.parse(event.detail.xhr.responseText) so quick action toast notifications actually fire - plugins_manager.js: add guarded htmx:afterSettle listener that only calls attachInstallButtonHandler when #install-plugin-from-url is in the DOM, eliminating the spurious console warning on non-plugin tab loads Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(web-ui): ensure quick-action toasts always fire even on xhr/parse failure Replace silent catch(e){} in all 11 hx-on:htmx:after-request handlers with a pattern that sets default message/status before the try block and calls showNotification(m,s) unconditionally after it, so a fallback toast is shown whenever xhr is absent or responseText is not valid JSON. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(web-ui): show error toast on non-JSON 4xx/5xx quick-action responses In the catch block of all 11 hx-on:htmx:after-request handlers, check xhr.status >= 400 and downgrade s to 'error' so a failed action that returns an HTML error page (or other non-JSON body) surfaces as an error toast instead of the optimistic 'success'/'info' default. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(web-ui): guard setTimeout fallback for attachInstallButtonHandler The 500ms fallback setTimeout was calling attachInstallButtonHandler() unconditionally even when the plugins partial wasn't in the DOM, causing a spurious console.warn on every page load. Add the same element-existence check already present on the htmx:afterSettle listener. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix backup API 404s, hardware status 500, and HTMX loading race - Add all backup API routes to api_v3.py: preview, list, export, validate, restore (with plugin reinstall), download, delete - Fix PermissionError on /hardware/status: return graceful 200 instead of 500 when the status file is owned by a different user; also fix root cause by writing the file world-readable (0o644) in display_manager - Fix HTMX race: dispatch htmx:ready window event from HTMX onload callback; loadTabContent now waits for that event instead of immediately falling back to direct fetch (eliminating the "HTMX not available" console warning on initial load) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Cancel HTMX fallback timers when htmx:ready fires The 5-second setTimeout fallbacks for plugins and overview were firing before the htmx:ready event arrived, logging spurious warnings. Each timer now self-cancels via htmx:ready so the fallback only triggers when HTMX genuinely fails to load. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Address review feedback: error leaks, ok:false, htmx:ready coverage - Backup endpoints: replace raw str(e) in user-facing responses with a generic message; full exception still logged via exc_info=True - hardware/status: change ok:null to ok:false for PermissionError and json.JSONDecodeError so the UI's hw.ok===false check triggers correctly - base.html: dispatch htmx:ready from the fallback load path so any deferred listeners fire on CDN-fallback loads too - loadTabContent: also listen for htmx-load-failed so overview/wifi/plugins fall back to direct fetch when HTMX is completely unavailable Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Treat system-managed pip packages as satisfied for dependency marker When a plugin's requirements.txt includes a package installed via the system package manager (dnf/apt), pip fails with 'uninstall-no-record-file' because it can't replace the system-tracked copy. The package is present and functional, but the missing marker caused the install to be retried on every service restart. Detect this specific error pattern: if the only pip failure is uninstall-no-record-file, write the .dependencies_installed marker and log a warning instead of returning False, suppressing the repeated warning. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix uninstall-no-record-file detection condition The previous check used a string replacement that left 'error:' in the remaining text, causing the condition to always evaluate false. Simplify to a direct substring check: if 'uninstall-no-record-file' appears in pip stderr the affected package is installed at the system level and we write the marker, suppressing the repeated warning on every restart. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Resolve CodeQL security findings in backup API Path traversal (CWE-22): - backup_download: switch from send_file(user-tainted-path) to send_from_directory(_BACKUP_EXPORT_DIR, filename); Flask uses werkzeug safe_join internally which CodeQL recognises as a sanitizer - backup_delete: enumerate the export directory and match by name so entry.unlink() operates on a filesystem-derived Path rather than one constructed from user input; _safe_backup_path still guards first Information exposure through exceptions (CWE-209): - backup_validate: err_msg from validate_backup() can embed exception strings containing temp-file paths; log the detail, return a generic 'Invalid or corrupted backup file' to the client - Other backup endpoints: already fixed (str(e) -> generic message); CodeQL alerts will clear on next scan plugin_loader.py:185 (path traversal): false positive — requirements_file is constructed from plugin_dir returned by find_plugin_directory() (a filesystem scan), not from raw HTTP request input; no change needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix pre-existing information exposure in version and action endpoints - get_system_version (alert #218): replaced str(e) with generic message; exception still logged via logger.error(exc_info=True) - execute_system_action (alert #216): removed str(e) and full traceback.format_exc() from the HTTP response — the full stack trace was being sent directly to clients; replaced with generic message and proper logger.error call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix remaining GitHub CodeQL security alerts - py/stack-trace-exposure: Remove str(e) and traceback.format_exc() from all HTTP responses across api_v3.py, pages_v3.py, and app.py; replace with generic messages and logger.error(exc_info=True) - py/reflective-xss: Escape partial_name via markupsafe.escape in the load_partial 404 response - py/path-injection: Add regex validation of plugin_id before filesystem use in _load_plugin_config_partial - py/incomplete-url-substring-sanitization: Replace 'github.com' in substring checks with urlparse hostname comparison in store_manager.py - py/clear-text-logging-sensitive-data: Remove football-scoreboard debug prints and sensitive request-body prints from update endpoint - js/bad-tag-filter: Replace script-only regex in BaseWidget.sanitizeValue with DOM-based textContent stripping that removes all HTML - js/incomplete-sanitization: Fix escapeAttr to properly encode &, ", ', <, > using HTML entities instead of backslash escaping - js/prototype-pollution-utility: Add __proto__/constructor/prototype key guards to deepMerge function in plugins_manager.js - app.py error handlers: Always return generic messages; remove debug-mode branches that could expose tracebacks in production Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix three remaining CodeQL path-injection and info-exposure alerts - plugin_loader.py: resolve plugin_dir with strict=True and validate marker_path with relative_to() before any filesystem writes, giving CodeQL the positive sanitization pattern it requires (py/path-injection) - api_v3.py _safe_backup_path: replace substring negative checks with a strict positive regex (^[a-zA-Z0-9][a-zA-Z0-9._-]{0,200}\.zip$) that CodeQL recognises as sanitising the user-supplied filename (py/path-injection) - api_v3.py backup_validate: whitelist known-safe manifest fields before returning JSON, preventing any exception strings captured inside validate_backup() from reaching the HTTP response (py/stack-trace-exposure) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Resolve 29 open CodeQL security alerts across 5 files py/flask-debug (#214): - debug_web_manual.py: read debug mode from LEDMATRIX_FLASK_DEBUG env var instead of hardcoded True py/stack-trace-exposure (#216, #218): - api_v3.py execute_system_action: remove subprocess stdout/stderr from HTTP responses; log via logger instead - api_v3.py get_git_version: validate output matches safe ref format (^[a-zA-Z0-9._-]+$) before including in response - api_v3.py: remove all remaining traceback.format_exc() dead variables and print() debug calls (replaced with logger.debug/warning) py/reflective-xss (#207, #208, #209, #210, #211, #212): - api_v3.py: remove plugin_id from all error/success response messages (uninstall, install, update, health, not-found responses) - pages_v3.py load_partial: return static "Partial not found" message instead of echoing partial_name - pages_v3.py _load_starlark_config_partial: add app_id regex validation, use static error messages instead of f-strings with app_id py/path-injection (#187–#206): - pages_v3.py _load_plugin_config_partial: resolve plugins_base and validate _plugin_dir with relative_to() before all file operations; same for assets metadata directory - pages_v3.py _load_starlark_config_partial: resolve starlark_base and validate schema_file/config_file paths with relative_to() - plugin_loader.py _find_plugin_directory: resolve plugins_dir and validate strategy-2 candidates with relative_to() - plugin_loader.py install_dependencies: resolve plugin_dir first, then construct requirements_file and marker_path from resolved base - plugin_loader.py load_module: resolve plugin_dir with strict=True and validate entry_file with relative_to() before exec_module Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix 15 remaining CodeQL path-injection and stack-trace-exposure alerts Switch from resolve()+relative_to() to os.path.basename() reassignment, which CodeQL recognizes as a path sanitizer that breaks the taint chain. Also remove exception objects from backup_manager validate_backup return strings to eliminate the stack-trace-exposure taint source. Fixes alerts #227, #233, #234, #235, #237, #238, #239, #240, #241, #242, #243, #244, #245, #246, #247. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix broken logger format string and leaked exception in config save error - pages_v3.py: plain string was used instead of %-style substitution, so every manifest-read failure logged the literal "{plugin_id}" - api_v3.py save_main_config: exception message was still leaking through the error response; replace with generic message (consistent with the rest of the CodeQL sweep in this PR) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
381 lines
19 KiB
HTML
381 lines
19 KiB
HTML
<!-- Reconciliation warning banner: shown when startup reconciliation found stale plugin config entries -->
|
|
<div id="reconciliation-banner" class="bg-yellow-50 border border-yellow-300 rounded-lg p-4 mb-4 flex items-start" style="display:none !important" role="alert">
|
|
<div class="flex-shrink-0 mr-3 mt-0.5">
|
|
<i class="fas fa-exclamation-triangle text-yellow-500"></i>
|
|
</div>
|
|
<div class="flex-1">
|
|
<p class="text-sm font-medium text-yellow-800">Plugin Config Warning</p>
|
|
<p class="text-sm text-yellow-700 mt-1" id="reconciliation-banner-text"></p>
|
|
</div>
|
|
<button type="button" onclick="window.dismissReconciliationBanner()" class="ml-4 flex-shrink-0 text-yellow-500 hover:text-yellow-700" aria-label="Dismiss">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<script>
|
|
(function () {
|
|
var DISMISS_KEY = 'ledmatrix-recon-dismissed';
|
|
var _recon_timer = null;
|
|
|
|
function checkReconciliation() {
|
|
fetch('/api/v3/plugins/reconciliation-status')
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (resp) {
|
|
var d = resp.data || {};
|
|
if (!d.done) {
|
|
// Reconciliation still running — poll again shortly
|
|
_recon_timer = setTimeout(checkReconciliation, 2000);
|
|
return;
|
|
}
|
|
_recon_timer = null;
|
|
if (!d.unresolved || d.unresolved.length === 0) return;
|
|
var key = d.unresolved.map(function (i) { return i.plugin_id; }).sort().join(',');
|
|
if (sessionStorage.getItem(DISMISS_KEY) === key) return;
|
|
var ids = d.unresolved.map(function (i) { return i.plugin_id; }).join(', ');
|
|
document.getElementById('reconciliation-banner-text').textContent =
|
|
'Stale plugin config entries found: ' + ids +
|
|
'. Remove them from config.json or reinstall via the Plugin Store.';
|
|
var banner = document.getElementById('reconciliation-banner');
|
|
banner.dataset.dismissKey = key;
|
|
banner.style.setProperty('display', 'flex', 'important');
|
|
})
|
|
.catch(function () {});
|
|
}
|
|
checkReconciliation();
|
|
|
|
window.dismissReconciliationBanner = function () {
|
|
var banner = document.getElementById('reconciliation-banner');
|
|
banner.style.setProperty('display', 'none', 'important');
|
|
if (_recon_timer !== null) {
|
|
clearTimeout(_recon_timer);
|
|
_recon_timer = null;
|
|
}
|
|
// Persist dismissal immediately so the banner won't reappear on reload
|
|
// even if the background sync fetch below fails.
|
|
var key = banner.dataset.dismissKey;
|
|
if (key) {
|
|
try { sessionStorage.setItem(DISMISS_KEY, key); } catch (e) {}
|
|
}
|
|
// Background sync only — do not rely on this for DISMISS_KEY or hiding.
|
|
fetch('/api/v3/plugins/reconciliation-status').catch(function () {});
|
|
};
|
|
}());
|
|
</script>
|
|
|
|
<div class="bg-white rounded-lg shadow p-6">
|
|
<div class="border-b border-gray-200 pb-4 mb-6">
|
|
<h2 class="text-lg font-semibold text-gray-900">System Overview</h2>
|
|
<p class="mt-1 text-sm text-gray-600">Monitor system status and manage your LED matrix display.</p>
|
|
</div>
|
|
|
|
<!-- System Stats Cards -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-4 2xl:grid-cols-4 gap-6 mb-8">
|
|
<div class="bg-gray-50 rounded-lg p-4">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<i class="fas fa-microchip text-blue-600 text-xl"></i>
|
|
</div>
|
|
<div class="ml-3 w-0 flex-1">
|
|
<dl>
|
|
<dt class="text-sm font-medium text-gray-500 truncate">CPU Usage</dt>
|
|
<dd class="text-lg font-medium text-gray-900" id="cpu-usage">--%</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-gray-50 rounded-lg p-4">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<i class="fas fa-memory text-green-600 text-xl"></i>
|
|
</div>
|
|
<div class="ml-3 w-0 flex-1">
|
|
<dl>
|
|
<dt class="text-sm font-medium text-gray-500 truncate">Memory Usage</dt>
|
|
<dd class="text-lg font-medium text-gray-900" id="memory-usage">--%</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-gray-50 rounded-lg p-4">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<i class="fas fa-thermometer-half text-red-600 text-xl"></i>
|
|
</div>
|
|
<div class="ml-3 w-0 flex-1">
|
|
<dl>
|
|
<dt class="text-sm font-medium text-gray-500 truncate">CPU Temperature</dt>
|
|
<dd class="text-lg font-medium text-gray-900" id="cpu-temp">--°C</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-gray-50 rounded-lg p-4">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<i class="fas fa-desktop text-purple-600 text-xl"></i>
|
|
</div>
|
|
<div class="ml-3 w-0 flex-1">
|
|
<dl>
|
|
<dt class="text-sm font-medium text-gray-500 truncate">Display Status</dt>
|
|
<dd class="text-lg font-medium text-gray-900" id="display-status">Unknown</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Version Info -->
|
|
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center">
|
|
<i class="fas fa-code-branch text-blue-600 text-xl mr-3"></i>
|
|
<div>
|
|
<dt class="text-sm font-medium text-gray-600">LEDMatrix Version</dt>
|
|
<dd class="text-lg font-semibold text-gray-900" id="ledmatrix-version">Loading...</dd>
|
|
</div>
|
|
</div>
|
|
<button onclick="checkForUpdates()"
|
|
class="inline-flex items-center px-3 py-2 border border-blue-300 text-sm font-medium rounded-md text-blue-700 bg-white hover:bg-blue-50">
|
|
<i class="fas fa-sync-alt mr-2"></i>
|
|
Check Updates
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Actions -->
|
|
<div class="border-b border-gray-200 pb-4 mb-6">
|
|
<h3 class="text-md font-medium text-gray-900 mb-4">Quick Actions</h3>
|
|
<div class="flex flex-wrap gap-3" hx-ext="json-enc">
|
|
<button hx-post="/api/v3/system/action"
|
|
hx-vals='{"action": "start_display"}'
|
|
hx-swap="none"
|
|
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">
|
|
<i class="fas fa-play mr-2"></i>
|
|
Start Display
|
|
</button>
|
|
|
|
<button hx-post="/api/v3/system/action"
|
|
hx-vals='{"action": "stop_display"}'
|
|
hx-swap="none"
|
|
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">
|
|
<i class="fas fa-stop mr-2"></i>
|
|
Stop Display
|
|
</button>
|
|
|
|
<button hx-post="/api/v3/system/action"
|
|
hx-vals='{"action": "git_pull"}'
|
|
hx-confirm="This will stash any local changes and update the code. Continue?"
|
|
hx-swap="none"
|
|
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">
|
|
<i class="fas fa-download mr-2"></i>
|
|
Update Code
|
|
</button>
|
|
|
|
<button hx-post="/api/v3/system/action"
|
|
hx-vals='{"action": "reboot_system"}'
|
|
hx-confirm="Are you sure you want to reboot the system?"
|
|
hx-swap="none"
|
|
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">
|
|
<i class="fas fa-power-off mr-2"></i>
|
|
Reboot System
|
|
</button>
|
|
|
|
<button hx-post="/api/v3/system/action"
|
|
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-swap="none"
|
|
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">
|
|
<i class="fas fa-power-off mr-2"></i>
|
|
Shutdown System
|
|
</button>
|
|
|
|
<button hx-post="/api/v3/system/action"
|
|
hx-vals='{"action": "restart_display_service"}'
|
|
hx-swap="none"
|
|
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">
|
|
<i class="fas fa-redo mr-2"></i>
|
|
Restart Display Service
|
|
</button>
|
|
|
|
<button hx-post="/api/v3/system/action"
|
|
hx-vals='{"action": "restart_web_service"}'
|
|
hx-swap="none"
|
|
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">
|
|
<i class="fas fa-redo mr-2"></i>
|
|
Restart Web Service
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Display Preview -->
|
|
<div>
|
|
<h3 class="text-md font-medium text-gray-900 mb-4">
|
|
<i class="fas fa-desktop"></i> Live Display Preview
|
|
</h3>
|
|
<div class="bg-gray-900 rounded-lg p-6 border border-gray-700" style="position: relative;">
|
|
<div id="previewStage" class="preview-stage" style="display:none; position:relative; display:inline-block;">
|
|
<div id="previewMeta" style="position:absolute; top:-28px; left:0; color:#ddd; font-size:12px; opacity:0.85;"></div>
|
|
<img id="displayImage" style="image-rendering: pixelated; display: block;" alt="LED Matrix Display">
|
|
<canvas id="ledCanvas" style="position:absolute; top:0; left:0; pointer-events:none; display:none; z-index: 10;"></canvas>
|
|
<canvas id="gridOverlay" style="position:absolute; top:0; left:0; pointer-events:none; z-index: 20;"></canvas>
|
|
</div>
|
|
<div id="displayPlaceholder" class="text-center text-gray-400 py-8">
|
|
<i class="fas fa-spinner fa-spin text-4xl mb-3"></i>
|
|
<p>Connecting to display...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Display Controls -->
|
|
<div class="mt-4 flex flex-wrap items-center gap-3">
|
|
<button onclick="takeScreenshot()" class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-900 bg-white hover:bg-gray-50">
|
|
<i class="fas fa-camera mr-2"></i> Screenshot
|
|
</button>
|
|
|
|
<label class="inline-flex items-center gap-2 bg-gray-100 px-3 py-2 rounded-md text-sm">
|
|
<span>Scale:</span>
|
|
<input type="range" id="scaleRange" min="2" max="16" value="8" class="w-20">
|
|
<span id="scaleValue" class="font-medium">8x</span>
|
|
</label>
|
|
|
|
<label class="inline-flex items-center gap-2 bg-gray-100 px-3 py-2 rounded-md text-sm cursor-pointer">
|
|
<input type="checkbox" id="toggleGrid" class="rounded">
|
|
<span>Show pixel grid</span>
|
|
</label>
|
|
|
|
<label class="inline-flex items-center gap-2 bg-gray-100 px-3 py-2 rounded-md text-sm cursor-pointer">
|
|
<input type="checkbox" id="toggleLedDots" checked class="rounded">
|
|
<span>LED dot mode</span>
|
|
</label>
|
|
|
|
<label class="inline-flex items-center gap-2 bg-gray-100 px-3 py-2 rounded-md text-sm">
|
|
<span>Dot fill:</span>
|
|
<input type="range" id="dotFillRange" min="40" max="95" value="75" class="w-16">
|
|
<span id="dotFillValue" class="font-medium">75%</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stats are updated via the global updateSystemStats function in base.html -->
|
|
|
|
<script>
|
|
// Load LEDMatrix version
|
|
(function() {
|
|
fetch('/api/v3/system/version')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const versionEl = document.getElementById('ledmatrix-version');
|
|
if (versionEl && data.status === 'success') {
|
|
versionEl.textContent = data.data.version;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
const versionEl = document.getElementById('ledmatrix-version');
|
|
if (versionEl) versionEl.textContent = 'Unknown';
|
|
});
|
|
})();
|
|
|
|
// Check for updates function
|
|
window.checkForUpdates = function() {
|
|
const btn = event.target.closest('button');
|
|
if (btn) {
|
|
const originalContent = btn.innerHTML;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Checking...';
|
|
|
|
fetch('/api/v3/system/action', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action: 'git_pull' })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
btn.innerHTML = originalContent;
|
|
btn.disabled = false;
|
|
|
|
if (data.status === 'success') {
|
|
showNotification('Update successful: ' + (data.stdout || 'Code updated'), 'success');
|
|
// Reload version after a short delay
|
|
setTimeout(() => {
|
|
fetch('/api/v3/system/version')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const versionEl = document.getElementById('ledmatrix-version');
|
|
if (versionEl && data.status === 'success') {
|
|
versionEl.textContent = data.data.version;
|
|
}
|
|
});
|
|
}, 1000);
|
|
} else {
|
|
showNotification('Update failed: ' + (data.stderr || data.message || 'Unknown error'), 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
btn.innerHTML = originalContent;
|
|
btn.disabled = false;
|
|
showNotification('Error checking for updates: ' + error.message, 'error');
|
|
});
|
|
}
|
|
};
|
|
|
|
// Setup display preview controls (runs when this partial loads)
|
|
(function() {
|
|
const scaleRange = document.getElementById('scaleRange');
|
|
const scaleValue = document.getElementById('scaleValue');
|
|
const dotFillRange = document.getElementById('dotFillRange');
|
|
const dotFillValue = document.getElementById('dotFillValue');
|
|
const toggleGrid = document.getElementById('toggleGrid');
|
|
const toggleLedDots = document.getElementById('toggleLedDots');
|
|
|
|
if (scaleRange && scaleValue) {
|
|
scaleRange.addEventListener('input', function() {
|
|
scaleValue.textContent = this.value + 'x';
|
|
// Re-render the preview with new scale
|
|
const img = document.getElementById('displayImage');
|
|
if (img && img.src) {
|
|
const data = {
|
|
image: img.src.replace('data:image/png;base64,', ''),
|
|
width: img.naturalWidth,
|
|
height: img.naturalHeight
|
|
};
|
|
updateDisplayPreview(data);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (dotFillRange && dotFillValue) {
|
|
dotFillRange.addEventListener('input', function() {
|
|
dotFillValue.textContent = this.value + '%';
|
|
renderLedDots();
|
|
});
|
|
}
|
|
|
|
if (toggleGrid) {
|
|
toggleGrid.addEventListener('change', function() {
|
|
const canvas = document.getElementById('gridOverlay');
|
|
const img = document.getElementById('displayImage');
|
|
if (canvas && img && img.src) {
|
|
const scale = parseInt(scaleRange?.value || '8');
|
|
drawGrid(canvas, img.naturalWidth, img.naturalHeight, scale);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (toggleLedDots) {
|
|
toggleLedDots.addEventListener('change', function() {
|
|
renderLedDots();
|
|
});
|
|
}
|
|
})();
|
|
</script>
|
|
|