Files
LEDMatrix/web_interface/templates/v3/partials/durations.html
T
ChuckandClaude Opus 5.5 342e9164b8 fix: settings the display ignored, a memory leak, and the plugin card handler (#605)
* fix(errors): stop affected_plugins growing without bound

Each repeat of an error pattern appended every plugin in the time window to
the pattern's list again, so a plugin failing in a loop grew the display
process's memory without limit: 3,000 errors from three plugins reached 2.5
million entries. Keep the list unique.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(fonts): load a BDF font at its native size instead of PIL's default

FreeType rejects any size but a BDF strike's own, and FontManager answered
that with ImageFont.load_default() -- a different typeface -- so 5x7.bdf
requested at 8 or 10px rendered as PIL's default font. Retry at the native
strike, as element_style already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(web): plugin toggle failures no longer claim "operation in progress"

Every exception in POST /plugins/toggle was mapped to
PLUGIN_OPERATION_CONFLICT, so any failure told the user "A plugin operation
is already in progress". Report the failure as what it is, and record the
plugin id in the operation history for form posts too (it read a `data`
variable that only the JSON path set).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(web): route plugin card clicks through handlePluginAction

The document-level delegation checked `typeof handlePluginAction`, which is
scoped inside the plugin-manager IIFE and so never visible to it. Every card
click took a copied fallback that stopped propagation (the grid's own
listener never ran), confirmed an uninstall twice, and sent Starlark app
uninstalls to POST /plugins/uninstall instead of DELETE /starlark/apps/<id>.
Expose the handler on window and delegate to it.

Also run every test/js/unit suite under pytest: they need only node, but CI
ran one of the eight.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(display): apply Rotation durations, WiFi messages and Vegas settings

Three settings the web UI saves never reached the display:

- Rotation & Durations: display.display_durations was never read. Every
  plugin inherits get_display_duration() and the plugin was asked first. A
  saved value now wins. The page shows unsaved screens blank with the
  plugin's own duration as a placeholder, and saving a blank removes the
  override, so one save no longer pins every screen.
- WiFi status overlay: the controller looked for wifi_status.json one
  directory above the repo. Both sides now use
  wifi_manager.get_wifi_status_path(). The message is written by rename so
  the display never reads it half-written, and the resumed plugin redraws the
  whole panel afterwards.
- Vegas: nothing called coordinator.update_config(), so saved Vegas settings
  never reached a running scroll. They are now queued when
  display.vegas_scroll changes, and applied while Vegas is stopped too, so a
  disable then re-enable works. The follower's scroll-speed default (75) now
  matches VegasModeConfig's (50).

Also throttles Vegas's per-frame live-priority scan to 4Hz. It cost 139us
per frame on a Pi 4 with two scoreboards (1.7% of a 125fps frame) and grows
with each plugin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep affected_plugins order when serialized; guard non-Element targets

ErrorPattern.to_dict() ran the now-ordered list through set(), so
get_error_summary() listed plugins in an unstable order. The document-level
card-action listener called event.target.closest() without checking the
target is an Element.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 10:29:16 -04:00

111 lines
5.7 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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 &amp; 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;">
{# See general.html: identifies a whole-form post to /config/main. #}
<input type="hidden" name="__form_section" value="durations">
<!-- 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&ndash;600). Leave a screen blank to use its plugin's own duration, shown greyed out.</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.' ~ (' Blank uses the plugin\'s own ' ~ mode.default ~ 's.' if mode.default else ''), mode.key | replace('_', ' ') | title) }}
</label>
<input type="number"
id="duration__{{ mode.key }}"
name="duration__{{ mode.key }}"
value="{{ mode.value }}"
{% if mode.default %}placeholder="{{ mode.default }}"{% endif %}
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 &mdash; 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>