fix(display): on-demand loads a disabled plugin live instead of failing (#678)

* fix(web): on-demand no longer restarts a running display service

POST /display/on-demand/start treated start_service (default true, sent by
"Preview on display", the on-demand dialog and the MQTT bridge) as
"restart": with the service running it ran systemctl stop, slept 1.5s and
started it again. Every request cold-started the display process -- every
plugin reloaded, panel blank -- to deliver a request the running process
already reads from the cache mailbox every ON_DEMAND_POLL_INTERVAL (0.25s),
including mid-dwell, mid-screen and mid-Vegas. The restart bought nothing:
startup only restores a session the display saved itself
(display_on_demand_config), so the new request arrived through the same
mailbox either way.

start_service now means "start it if it is not running". The stop route
coerces stop_service to a boolean so "false" no longer stops the service.
test_api_v3_on_demand_restart.py pinned the old restart path; it now pins
the replacement. Docs updated.

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

* fix(display): on-demand loads a disabled plugin live instead of failing

The display process only loads enabled plugins, so an on-demand request for
a disabled one -- "Preview on display" offers it on every config page, with a
note that the plugin will be enabled for the preview -- failed with
invalid-mode. Nothing enabled it short of a restart, and the on-demand route
no longer restarts the service.

_activate_on_demand now loads an installed-but-not-running plugin through
the live-enable path (load_plugin + _register_loaded_plugin), with a new
load_plugin(force_enabled=True) so the instance runs enabled while
config.json keeps saying disabled. The plugin is tracked in
_on_demand_loaded_plugins, and the main loop unloads it through
_unregister_plugin once on-demand moves off it (stop, expiry, another
request, or a failed request that ends the session) -- right after its own
poll, where no display() is on the stack. A failed load publishes status
error with load-failed. A plugin enabled during the session stays loaded.

A session restored after a restart uses the same tracking instead of
setting enabled in the config dict config_manager caches, so its plugin is
unloaded when the session ends rather than staying loaded until the next
restart. Ending a session no longer resumes the rotation onto a plugin that
is about to be unloaded, which a restored session did.

Also: a stop sent while on-demand is inactive clears a failed request's
error, instead of /display/on-demand/status reporting status: error until
the state aged out.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-29 17:59:12 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 6047eb5e4e
commit c3a7a110c4
7 changed files with 654 additions and 28 deletions
+11 -1
View File
@@ -296,7 +296,7 @@ class PluginManager:
return plugin_ids
def load_plugin(self, plugin_id: str) -> bool:
def load_plugin(self, plugin_id: str, force_enabled: bool = False) -> bool:
"""
Load a plugin by ID.
@@ -310,6 +310,10 @@ class PluginManager:
Args:
plugin_id: Plugin identifier
force_enabled: Run the plugin enabled even though config.json has
it disabled. On-demand uses this to show a disabled plugin
(DisplayController._load_plugin_for_on_demand). Only the
instance's config says enabled; config.json is not written.
Returns:
True if loaded successfully, False otherwise
@@ -376,6 +380,12 @@ class PluginManager:
# (prepare_plugin_config). In memory only: config.json is written
# by saves, never by loading a plugin.
config = self.prepare_plugin_config(plugin_id, config, schema=schema)
if force_enabled:
# A copy: prepare_plugin_config can hand back the section from
# config_manager's cached config, and setting the flag there
# would read as enabled to everything else in this process.
config = dict(config)
config['enabled'] = True
# Use PluginLoader to load plugin
plugin_instance, _module = self.plugin_loader.load_plugin(