mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-01 08:48:05 +00:00
Verified each finding against current code. Fixed: - api_v3: plugin_rotation_order is now strictly validated (JSON list of strings, 400 with a descriptive message otherwise) and popped from the payload before any further handling. - display_controller: _apply_plugin_rotation_order defensively ignores a non-list value (keeps the existing rotation, logs a warning) and drops non-string entries; new logs carry the [DisplayController] prefix. Unit-tested both defensive paths. - app.py: snapshot-read handler narrowed to OSError with debug logging; flask-compress ImportError now emits one structured warning with the install remedy. - htmx-config: the response-error logger prints form FIELD NAMES only - values (API keys, passwords) never reach the console. - plugin-order-list: saved order/exclusions normalized with Array.isArray (a saved "null" previously crashed .forEach); each row gained keyboard/touch-accessible move-up/move-down buttons (HTML5 drag events don't fire on most mobile browsers) that reorder and syncInputs() immediately alongside native drag. - app-shell: window.installedPlugins setter always takes the new list (same-ID metadata/enabled updates were silently dropped); tab rebuild stays gated on ID changes. LED dot renderer reads the frame with ONE getImageData call instead of one per pixel (~9,200/frame at 192x48). - plugins_manager: togglePlugin returns its request promise resolving the API outcome; the install flow now shows the "installed and enabled" toast (with Restart Now) only after enablement succeeds, and a warning without a restart offer when it fails. - a11y: hamburger aria-label flips Open/Close with drawer state; both Advanced-section toggle buttons declare aria-controls/aria-expanded and the shared toggleSection() keeps aria-expanded in sync; move buttons have per-plugin aria-labels. - Rotation/Vegas order-list bootstraps cap their retries (~5s) and show a reload hint instead of spinning forever; Alpine app-state lookups prefer [x-data="app()"] with a generic fallback. Skipped, with reasons: - executePluginAction arg order: caller (plugin_config.html) already passes (actionId, index, pluginId) matching the signature exactly. - generateFieldHtml XSS, entity-unescape blocks, dotToNested pollution, and "app.loadInstalledPlugins" in app-shell: all inside the legacy client-side config cluster whose entry points are shadowed by plugins_manager.js / replaced by server-rendered forms (zero live callers, verified) - queued for wholesale deletion in the follow-up rather than patching dead code. - custom-feeds-helpers.js findings (3): file was deleted in a prior commit. - console.error/warn override removal and afterSwap script re-execution removal: deliberate pre-existing workarounds every partial's inline init currently depends on; reworking them safely needs isolated testing (follow-up), and the error suppression is already double-gated (insertBefore AND htmx match). - "move durations bootstrap into a bundle": inline partial-scoped init is the established pattern for HTMX partials in this codebase. Validation: all 40 web tests pass; py_compile on all touched Python; all touched templates parse; rotation-order defensive paths unit-tested. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
108 lines
5.3 KiB
HTML
108 lines
5.3 KiB
HTML
{% import 'v3/partials/_macros.html' as ui %}
|
||
<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">Rotation & Durations</h2>
|
||
<p class="mt-1 text-sm text-gray-600">Set the order plugins rotate on the display and how long each screen is shown. Durations are in seconds.</p>
|
||
</div>
|
||
|
||
{{ ui.settings_filter() }}
|
||
|
||
<form hx-post="/api/v3/config/main"
|
||
hx-ext="json-enc"
|
||
hx-headers='{"Content-Type": "application/json"}'
|
||
hx-swap="none"
|
||
hx-on:htmx:after-request="showNotification(event.detail.xhr.responseJSON?.message || 'Durations saved', event.detail.xhr.responseJSON?.status || 'success')"
|
||
class="space-y-6"
|
||
novalidate
|
||
onsubmit="fixInvalidNumberInputs(this); return true;">
|
||
|
||
<!-- Primary rotation order: drag to reorder which plugin shows first,
|
||
second, ... in the normal display rotation. Saved as
|
||
display.plugin_rotation_order and applied by the display
|
||
controller on startup and live plugin enable/disable. -->
|
||
<div class="bg-gray-50 rounded-lg p-4">
|
||
<h3 class="text-md font-medium text-gray-900 mb-1">Rotation Order</h3>
|
||
<p class="text-sm text-gray-600 mb-3">Drag plugins to set the order they rotate on the display. Each plugin's screens keep their own order within its turn. Takes effect after saving and restarting the display.</p>
|
||
<div id="rotation_plugin_order" class="space-y-2 bg-white rounded-lg p-3 border border-gray-200">
|
||
<p class="text-sm text-gray-500 italic">Loading plugins…</p>
|
||
</div>
|
||
<input type="hidden" id="rotation_plugin_order_value" name="plugin_rotation_order"
|
||
value='{{ main_config.display.get("plugin_rotation_order", [])|tojson }}'>
|
||
</div>
|
||
|
||
{% if duration_groups %}
|
||
<div class="bg-gray-50 rounded-lg p-4 space-y-5">
|
||
<div>
|
||
<h3 class="text-md font-medium text-gray-900 mb-1">Screen Durations</h3>
|
||
<p class="text-sm text-gray-600">How long each screen stays on before rotating to the next one, in seconds (5–600, default 30).</p>
|
||
</div>
|
||
{% for group in duration_groups %}
|
||
<div>
|
||
<h4 class="text-sm font-semibold text-gray-800 mb-2">{{ group.plugin_name }}</h4>
|
||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||
{% for mode in group.modes %}
|
||
<div class="form-group" id="setting-durations-{{ mode.key }}" data-setting-key="display.display_durations.{{ mode.key }}">
|
||
<label for="duration__{{ mode.key }}" class="block text-sm font-medium text-gray-700">
|
||
{{ mode.key | replace('_', ' ') | title }}{{ ui.help_tip('How long the ' ~ (mode.key | replace('_', ' ')) ~ ' screen stays on before rotating to the next one, in seconds.\nRange: 5–600. Currently ' ~ mode.value ~ 's.', mode.key | replace('_', ' ') | title) }}
|
||
</label>
|
||
<input type="number"
|
||
id="duration__{{ mode.key }}"
|
||
name="duration__{{ mode.key }}"
|
||
value="{{ mode.value }}"
|
||
min="5"
|
||
max="600"
|
||
class="form-control">
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% else %}
|
||
<div class="bg-gray-50 rounded-lg p-4">
|
||
<p class="text-sm text-gray-500 italic">No enabled plugins found — enable a plugin in the Plugin Manager to set its screen durations here.</p>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Submit Button -->
|
||
<div class="flex justify-end">
|
||
<button type="submit"
|
||
class="btn bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md">
|
||
<i class="fas fa-save mr-2"></i>
|
||
Save Durations
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<script>
|
||
(function () {
|
||
// Shared drag-and-drop plugin list (static/v3/js/widgets/plugin-order-list.js,
|
||
// same module the Vegas Scroll section uses).
|
||
function initRotationOrderList(attempt) {
|
||
const container = document.getElementById('rotation_plugin_order');
|
||
if (!container) return;
|
||
if (!window.PluginOrderList) {
|
||
// Widget script is deferred; retry briefly, then surface a real
|
||
// error instead of showing "Loading…" forever.
|
||
if ((attempt || 0) < 50) {
|
||
setTimeout(function() { initRotationOrderList((attempt || 0) + 1); }, 100);
|
||
} else {
|
||
container.textContent = 'Could not load the reorder widget — reload the page to try again.';
|
||
container.className = 'text-sm text-red-500';
|
||
}
|
||
return;
|
||
}
|
||
window.PluginOrderList.init({
|
||
containerId: 'rotation_plugin_order',
|
||
orderInputId: 'rotation_plugin_order_value'
|
||
});
|
||
}
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', initRotationOrderList);
|
||
} else {
|
||
initRotationOrderList();
|
||
}
|
||
}());
|
||
</script>
|