Files
LEDMatrix/web_interface/templates/v3/partials/schedule.html
T
ChuckandClaude Opus 5.5 1fe7965f5c feat(schedule): per-plugin schedules and hide-while-live (#799)
* feat(schedule): per-plugin schedules and hide-while-live

Users asked to turn off news and stocks on gameday so only scores show.
Each plugin can now have its own schedule (global or per-day window in
which it may show) plus hide_when_live, which hides it whenever any
plugin reports a live game. A hidden plugin is skipped in the rotation,
the live takeover and the Vegas ticker; it stays loaded, and on-demand
still shows it.

- src/plugin_schedule.py: the window rule, now shared with the display
  schedule (_check_schedule calls schedule_state)
- core config key plugin_schedules; GET/POST /api/v3/config/plugin-schedules
- Schedule page: Plugin Schedules card, one picker per enabled plugin

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

* fix(schedule): tolerate wrong-shaped schedules, quiet skipped modes

- schedule_state treats a non-string mode, non-dict days or a non-dict
  day entry as missing instead of raising in the display loop
- the run loop no longer warns "No plugin manager found" for a mode it
  skips: _plugin_to_draw / _plugin_for_mode already log why (a
  scheduled-off plugin at DEBUG)
- Schedule page DOM test: every form is marked data-reports-result, not
  exactly two (each plugin adds one)

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

* fix(schedule): guard plugin_schedules shape and the cross-thread cache

- a plugin_schedules that is not a dict (list, string) hides nothing
  instead of raising in the display loop
- the hidden-set cache keys on the schedules themselves and is published
  as one tuple, so a Vegas-thread call still holding the pre-reload
  config cannot leave its answer standing after a reload; the separate
  invalidation in _refresh_config_cache is gone

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

* fix(schedule): no live hold on a scheduled-off mode; blank an empty rotation

- _still_live ignores a plugin its schedule hides, so a rotation landing
  on a scheduled-off live mode advances instead of holding a mode it skips
- once a whole rotation has had nothing to show (every plugin empty or
  scheduled off), the panel is cleared once instead of leaving the last
  screen up

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

* fix(schedule): push the empty-rotation blank to the web preview

clear() alone blanked the matrix but left the preview snapshot on the
last frame (seen on ledpi with every plugin scheduled off). Follow it
with update_display(), as the scheduled-off blank does.

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

* fix(schedule): write the empty-rotation blank to the preview

The preview throttle skipped the blank frame (written 12 s after the
last snapshot), and write_owed_snapshot() only runs after a display()
call, which an all-empty rotation never makes: on ledpi the preview
kept the startup splash while the panel was dark. Flush the owed
snapshot on each paused empty pass.

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

* fix(schedule): quiet the once-a-second logs when nothing can show

With every plugin scheduled off (seen on ledpi), the loop retries once
a second and journaled three to four lines each time: Vegas's
"No plugins available" WARNING, the coordinator's "Failed to initialize
stream manager" ERROR, the 0-plugin rotation summary and "Switching to
mode". Vegas now warns once until its list fills again, the coordinator
line is DEBUG (initialize only fails for an empty list), and the
mode-switch line is DEBUG once a whole rotation has been empty.

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

* fix(schedule): end the scroll before the empty blank; Vegas sees hidden changes sooner

- the empty-rotation blank is controller-drawn, so it ends the last
  scroll's state first (_end_scroll_before_core_screen), as the
  scheduled-off blank does
- StreamManager.refresh() rebuilds the plugin list when the hidden set
  changed, even inside its 30 s interval, so a newly hidden plugin leaves
  at the next scroll group rather than staying for later ones

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

---------

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

136 lines
6.4 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 %}
{# No inline script: static/v3/js/pages/schedule.js draws both schedule
pickers from the data-*-config attributes, reports each form's save and
keeps the dim brightness label current. The page registry
(static/v3/js/core/registry.js) starts it when this root appears and stops
it when the partial is swapped away. The forms carry data-reports-result so
app.js leaves the save notification to the page. #}
<div data-page="schedule"
data-schedule-config='{{ schedule_config | tojson }}'
data-dim-schedule-config='{{ dim_schedule_config | tojson }}'>
<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">Schedule Settings</h2>
<p class="mt-1 text-sm text-gray-600">Configure when the LED matrix display should be active. You can set global hours or customize times for each day of the week.</p>
</div>
{{ ui.settings_filter() }}
<form id="schedule_form"
hx-post="/api/v3/config/schedule"
hx-ext="json-enc"
hx-headers='{"Content-Type": "application/json"}'
hx-swap="none"
data-reports-result
class="space-y-6">
<!-- Schedule Picker Widget Container -->
<div id="schedule_picker_container"></div>
<!-- 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 Schedule Settings
</button>
</div>
</form>
</div>
<!-- Dim Schedule Section -->
<div class="bg-white rounded-lg shadow p-6 mt-6">
<div class="border-b border-gray-200 pb-4 mb-6">
<h2 class="text-lg font-semibold text-gray-900">Dim Schedule Settings</h2>
<p class="mt-1 text-sm text-gray-600">Configure when the display should automatically dim to a lower brightness. Dimming only applies when the display is ON (active per the schedule above). The dim schedule will not turn the display back on if it's off.</p>
</div>
<form id="dim_schedule_form"
hx-post="/api/v3/config/dim-schedule"
hx-ext="json-enc"
hx-headers='{"Content-Type": "application/json"}'
hx-swap="none"
data-reports-result
class="space-y-6">
<!-- Dim Brightness Level -->
<div class="form-group bg-gray-50 rounded-lg p-4 mb-4" id="setting-schedule-dim_brightness" data-setting-key="dim_schedule.dim_brightness">
<label for="dim_brightness" class="block text-sm font-medium text-gray-700 mb-2">
Dim Brightness Level{{ ui.help_tip('Brightness the display drops to during dim hours (0–100%).\nApplies only while the display is on. Your normal brightness is currently ' ~ normal_brightness ~ '%.', 'Dim Brightness Level') }}
</label>
<div class="flex items-center space-x-4">
<input type="range"
id="dim_brightness"
name="dim_brightness"
min="0"
max="100"
value="{{ dim_schedule_config.dim_brightness | default(30) }}"
class="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-600">
<span id="dim_brightness_display" class="text-sm font-medium text-gray-700 w-12 text-right">
{{ dim_schedule_config.dim_brightness | default(30) }}%
</span>
</div>
</div>
<!-- Dim Schedule Picker Widget Container -->
<div id="dim_schedule_picker_container"></div>
<!-- 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 Dim Schedule Settings
</button>
</div>
</form>
</div>
<!-- Plugin Schedules Section (config plugin_schedules, src/plugin_schedule.py) -->
<div class="bg-white rounded-lg shadow p-6 mt-6">
<div class="border-b border-gray-200 pb-4 mb-6">
<h2 class="text-lg font-semibold text-gray-900">Plugin Schedules</h2>
<p class="mt-1 text-sm text-gray-600">Choose when each plugin may show. Outside its schedule a plugin is skipped in the rotation and in Vegas mode; it stays loaded, and on-demand still shows it. "Hide while a game is live" skips it whenever any plugin has a live game, so on gameday you only see scores.</p>
</div>
{% if plugin_schedule_rows %}
<div class="space-y-3">
{% for row in plugin_schedule_rows %}
<details class="border border-gray-200 rounded-md">
<summary class="cursor-pointer px-4 py-3 text-sm font-medium text-gray-900">
{{ row.name }}
{% if row.schedule.enabled or row.schedule.hide_when_live %}
<span class="ml-2 text-xs text-blue-700">scheduled</span>
{% endif %}
</summary>
<form class="plugin-schedule-form px-4 pb-4 space-y-4"
hx-post="/api/v3/config/plugin-schedules"
hx-ext="json-enc"
hx-headers='{"Content-Type": "application/json"}'
hx-swap="none"
data-reports-result>
<input type="hidden" name="plugin_id" value="{{ row.id }}">
<label class="flex items-center text-sm text-gray-700">
<input type="checkbox" name="hide_when_live" class="mr-2"
{% if row.schedule.hide_when_live %}checked{% endif %}>
Hide while a game is live
</label>
<div id="plugin_schedule_picker_{{ loop.index }}"
data-plugin-schedule-config='{{ row.schedule | tojson }}'></div>
<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 {{ row.name }} Schedule
</button>
</div>
</form>
</details>
{% endfor %}
</div>
{% else %}
<p class="text-sm text-gray-600">No enabled plugins.</p>
{% endif %}
</div>
</div>