Files
Paperwall/web/templates/settings_rotation.html
T
Honey 9aacd4936a Add time-based screen scheduling (e.g. Journey every morning 6-8)
New "Time-based schedule" section on the Rotation settings page: up to
4 rules, each a screen + a daily HH:MM-HH:MM window (crossing midnight
is fine, e.g. 22:00-02:00). While a rule's window is active, that
screen overrides rotation and any manual pin -- without touching
either's underlying state, so things go back to exactly how they were
once the window ends. Navidrome-playing still wins on top of a
schedule rule, same as it already does over everything else.

config.py: new schedules list (get_schedules/set_schedules), each rule
just {screen_id, start, end} -- being in the list *is* being enabled,
no separate flag (a settings row is "disabled" by leaving its screen
dropdown on "-- none --").

scheduler.py: _time_in_window() handles same-day and midnight-wrapping
windows, treats a zero-width window as never-matching (more likely a
blank settings row than an intentional all-day rule).
_active_scheduled_screen() slots into _select_screen() between
Navidrome-priority and the pin/rotation fallback.

Verified: unit tests for _time_in_window's edge cases (wraparound,
zero-width, malformed input) and the priority override, plus an
end-to-end run against the real app -- pinned a screen, added a rule
active right now, confirmed the status page switched to the scheduled
screen within one tick.
2026-07-26 21:27:59 +02:00

75 lines
3.3 KiB
HTML

{% extends "base.html" %}
{% block content %}
<h1>Rotation</h1>
<form method="post">
<div class="card">
<div class="section-label">Priority</div>
<label class="checkbox">
<input type="checkbox" name="navidrome_priority" {{ "checked" if navidrome_priority }}>
Navidrome takes over immediately whenever something is playing, regardless of rotation
</label>
</div>
<div class="card">
<div class="section-label">Rotation</div>
<label class="checkbox">
<input type="checkbox" name="rotation_enabled" {{ "checked" if rotation.enabled }}>
Enabled -- cycle through the screens below on a timer
</label>
<p class="hint" style="margin: 0.6rem 0 1.1rem;">
Clicking "Show X now" on the Status page turns this off automatically
and fixes the display to that screen until you turn rotation back on
here (Navidrome priority above still applies on top of a pin, if
enabled). With no pin and rotation off, it just sits on the first
screen in the order below.
</p>
<div class="field">
<label for="screens">Order</label>
<input type="text" id="screens" name="screens" value="{{ rotation.screens | join(', ') }}" placeholder="clock, weather">
<p class="hint">Comma-separated, in order. Available: {{ screens.keys() | join(', ') }}.</p>
</div>
<div class="field">
<label for="interval_minutes">Switch every (minutes)</label>
<input type="number" id="interval_minutes" name="interval_minutes" value="{{ rotation.interval_minutes }}" min="1">
</div>
</div>
<div class="card">
<div class="section-label">Time-based schedule</div>
<p class="hint" style="margin: -0.25rem 0 1rem;">
Show a specific screen automatically during a daily time window --
handy for e.g. checking bus departures every morning. Takes priority
over rotation and any manual pin while active (Navidrome priority
above still wins over this if enabled), and reverts back on its own
once the window ends. A window can cross midnight (e.g. 22:00-02:00).
Leave a row's screen on "-- none --" to disable it.
</p>
{% for i in range(1, max_schedule_rules + 1) %}
{% set rule = schedules[i - 1] if i - 1 < schedules|length else None %}
<div class="field" style="display: flex; gap: 0.6rem; align-items: flex-end; flex-wrap: wrap;">
<div style="flex: 1; min-width: 160px;">
<label for="schedule_screen_{{ i }}">Screen</label>
<select id="schedule_screen_{{ i }}" name="schedule_screen_{{ i }}">
<option value="">-- none --</option>
{% for screen_id, screen in screens.items() | sort(attribute="1.display_name") %}
<option value="{{ screen_id }}" {{ "selected" if rule and rule.screen_id == screen_id }}>{{ screen.display_name }}</option>
{% endfor %}
</select>
</div>
<div>
<label for="schedule_start_{{ i }}">From</label>
<input type="time" id="schedule_start_{{ i }}" name="schedule_start_{{ i }}" value="{{ rule.start if rule else '' }}">
</div>
<div>
<label for="schedule_end_{{ i }}">To</label>
<input type="time" id="schedule_end_{{ i }}" name="schedule_end_{{ i }}" value="{{ rule.end if rule else '' }}">
</div>
</div>
{% endfor %}
</div>
<button type="submit">Save</button>
</form>
{% endblock %}