From 05aee7414790069a2504910b70140dfea704a35f Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Thu, 16 Jul 2026 12:09:48 -0400 Subject: [PATCH] fix(web): Rotation & Durations page lists every enabled plugin's screens The durations grid looped over display.display_durations, which nothing has ever populated (verified {} on a real production install) - so the page rendered no duration fields at all. Worse, its inputs posted bare mode names, which save_main_config's endswith('_duration') filter silently dropped: the page was broken in both directions, unnoticed because it was also unreachable (previous commit). - pages_v3._load_durations_partial now builds one entry per display mode of every ENABLED plugin via plugin_manager.get_plugin_display_modes() (falling back to the plugin id), overlaid with saved values, defaulting to the display controller's 30s. Grouped per plugin, sorted by name. Saved keys not owned by any enabled plugin stay visible under "Other saved entries" instead of vanishing. - durations.html renders the grouped inputs, named duration__ (mode keys are arbitrary, so they can't use the *_duration suffix convention), with an explanatory empty state when no plugins are enabled. - api_v3.save_main_config accepts the new duration__ fields and writes them into display.display_durations under the bare mode key - exactly what the display controller reads (display_durations.get(mode_key, 30)). Validation: py_compile both blueprints; Jinja render with 3 groups asserts grouped inputs, saved-value overlay, stale-entry group, empty state, and div balance. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ --- web_interface/blueprints/api_v3.py | 19 ++++++++ web_interface/blueprints/pages_v3.py | 43 ++++++++++++++++++- .../templates/v3/partials/durations.html | 43 +++++++++++++------ 3 files changed, 90 insertions(+), 15 deletions(-) diff --git a/web_interface/blueprints/api_v3.py b/web_interface/blueprints/api_v3.py index ec12be1b..0aca34cb 100644 --- a/web_interface/blueprints/api_v3.py +++ b/web_interface/blueprints/api_v3.py @@ -989,6 +989,25 @@ def save_main_config(): if field in data: current_config['display']['display_durations'][field] = int(data[field]) + # Per-mode durations from the Rotation & Durations page, posted as + # duration__ (mode keys are arbitrary plugin mode names, so + # they can't use the suffix convention above) + mode_duration_fields = [k for k in data.keys() if k.startswith('duration__')] + if mode_duration_fields: + if 'display' not in current_config: + current_config['display'] = {} + if 'display_durations' not in current_config['display']: + current_config['display']['display_durations'] = {} + + for field in mode_duration_fields: + mode_key = field[len('duration__'):] + if not mode_key: + continue + try: + current_config['display']['display_durations'][mode_key] = int(data[field]) + except (ValueError, TypeError): + logger.warning("Ignoring non-integer duration for %s", mode_key) + # Handle plugin configurations dynamically # Any key that matches a plugin ID should be saved as plugin config # This includes proper secret field handling from schema diff --git a/web_interface/blueprints/pages_v3.py b/web_interface/blueprints/pages_v3.py index 8c3c1bbc..f25752ce 100644 --- a/web_interface/blueprints/pages_v3.py +++ b/web_interface/blueprints/pages_v3.py @@ -397,12 +397,51 @@ def _load_display_partial(): return "Error loading partial", 500 def _load_durations_partial(): - """Load display durations partial""" + """Load rotation & durations partial. + + Builds one duration entry per display mode of every enabled plugin + (falling back to the display controller's 30s default), overlaid with any + values saved in display.display_durations. Historically the template only + looped over saved keys, and nothing ever populated them, so the page + rendered empty. + """ try: if pages_v3.config_manager: main_config = pages_v3.config_manager.load_config() + duration_groups = [] + covered_keys = set() + if pages_v3.plugin_manager: + try: + pages_v3.plugin_manager.discover_plugins() + saved = (main_config.get('display', {}) or {}).get('display_durations', {}) or {} + infos = sorted(pages_v3.plugin_manager.get_all_plugin_info(), + key=lambda i: (i.get('name') or i.get('id') or '').lower()) + for info in infos: + pid = info.get('id') + if not pid or not (main_config.get(pid, {}) or {}).get('enabled', False): + continue + modes = pages_v3.plugin_manager.get_plugin_display_modes(pid) or [pid] + covered_keys.update(modes) + duration_groups.append({ + 'plugin_id': pid, + 'plugin_name': info.get('name') or pid, + 'modes': [{'key': m, 'value': saved.get(m, 30)} for m in modes], + }) + # Saved keys not owned by any enabled plugin (disabled or + # uninstalled plugins) stay visible rather than vanishing. + leftovers = [{'key': k, 'value': v} for k, v in saved.items() + if k not in covered_keys] + if leftovers: + duration_groups.append({ + 'plugin_id': '', + 'plugin_name': 'Other saved entries', + 'modes': leftovers, + }) + except Exception: + logger.warning("durations: could not enumerate plugin modes", exc_info=True) return render_template('v3/partials/durations.html', - main_config=main_config) + main_config=main_config, + duration_groups=duration_groups) except Exception as e: logger.error("Error loading partial", exc_info=True) return "Error loading partial", 500 diff --git a/web_interface/templates/v3/partials/durations.html b/web_interface/templates/v3/partials/durations.html index d980744c..6c1515a5 100644 --- a/web_interface/templates/v3/partials/durations.html +++ b/web_interface/templates/v3/partials/durations.html @@ -30,22 +30,39 @@ value='{{ main_config.display.get("plugin_rotation_order", [])|tojson }}'> -
- {% for key, value in main_config.display.display_durations.items() %} -
- - + {% if duration_groups %} +
+
+

Screen Durations

+

How long each screen stays on before rotating to the next one, in seconds (5–600, default 30).

+
+ {% for group in duration_groups %} +
+

{{ group.plugin_name }}

+
+ {% for mode in group.modes %} +
+ + +
+ {% endfor %} +
{% endfor %}
+ {% else %} +
+

No enabled plugins found — enable a plugin in the Plugin Manager to set its screen durations here.

+
+ {% endif %}