fix(plugins): web mode lookups use the modes the display registered (#668)

A plugin may compute its display modes from its config: soccer-scoreboard
registers soccer_<league>_live/recent/upcoming for every custom_leagues
entry, which no manifest can list ahead of time. The display always rotated
them (_register_loaded_plugin prefers plugin.modes), but the web process
reads plugins as files, so /display/modes, the on-demand dialog and
on-demand/start with a mode and no plugin_id saw only manifests -- a custom
league's mode was missing from every list and 404'd on lookup.

- PluginStateManager.record_modes(): the controller records what it
  registered, on the loaded record (an unload or reload forgets it)
- the runtime snapshot carries it per plugin as "modes" (bounded), and
  PluginRuntimeView.display_modes() reports it only while live
- PluginCatalog takes a runtime_source; get_plugin_display_modes and
  find_plugin_for_mode prefer the live modes, falling back to the manifest
  when the display is stopped or has not loaded the plugin. The view is read
  at most once a second, so a listing is one read, not one per plugin.

No manifest or plugin change needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-10-05 08:33:47 -04:00
co-authored by Claude Opus 5.5
parent 3f920f2899
commit 554426e023
11 changed files with 405 additions and 22 deletions
+8
View File
@@ -159,10 +159,18 @@ schema_manager = SchemaManager(
# saves reach the running plugins through the display's config watcher; what
# the display knows at run time (health, metrics, errors, current mode) it
# publishes to the shared cache. See docs/ARCHITECTURE.md.
def _catalog_runtime_view():
"""The display's runtime view, for the catalog's mode lookups. Imported
on call, as the startup reconciliation below imports it."""
from web_interface.blueprints.api_v3 import _plugin_runtime_view
return _plugin_runtime_view()
plugin_catalog = PluginCatalog(
plugins_dir=plugins_dir,
config_manager=config_manager,
schema_manager=schema_manager,
runtime_source=_catalog_runtime_view,
)
# Initialize operation queue for plugin operations
+6 -4
View File
@@ -153,10 +153,12 @@ def get_display_modes():
same list the force-display dialog offers, from the source that owns it.
Knowing each mode's plugin_id also matters because /display/on-demand/start
falls back to find_plugin_for_mode when plugin_id is omitted, and that
lookup only sees modes declared in a static manifest -- a plugin whose
modes are generated (each installed Starlark app is one) 404s there.
Sending the plugin_id from this list skips the lookup entirely.
falls back to find_plugin_for_mode when plugin_id is omitted. While the
display is running, both that lookup and this list use the modes it
registered, so modes a plugin generates from its config (each installed
Starlark app, each soccer custom league) are found (#668); with the
display stopped they see only what manifests declare. Sending the
plugin_id from this list skips the lookup entirely.
Query params:
include_disabled: '1' to list modes of disabled plugins too. They can
+3 -2
View File
@@ -150,8 +150,9 @@ def get_installed_plugins():
vegas_participation, vegas_participation_source = _vegas_participation(
plugin_id, plugin_config, plugin_info)
# The modes the manifest declares, from the catalog as /display/modes
# and on-demand/start read them. The on-demand modal offers these;
# The plugin's modes, from the catalog as /display/modes and
# on-demand/start read them: what the running display registered,
# else what the manifest declares. The on-demand modal offers these;
# without them it offered only the plugin id, which the display
# turns into the first mode. Strings only: a manifest is hand-edited.
declared_modes = api_v3.plugin_catalog.get_plugin_display_modes(plugin_id)