Stage 2 of the web plugin catalog, after #688.
- The display publishes a plugin runtime snapshot (plugin_runtime.py) to
the shared cache: per plugin loaded, lifecycle state, a short redacted
error summary, the version it loaded and when, plus published_at /
stale_after / running. Written on change (throttled to 10 s; the
RUNNING/ENABLED flip of an ordinary update is not a change) and once a
minute otherwise; cleanup() publishes running: false.
- The web reads it back and restores loaded / state / error_info in
/api/v3/plugins/installed (plus loaded_version, loaded_at and
data.runtime). Only a live snapshot counts; stale, stopped or missing
answers null and says which.
- data/plugin_state.json is retired: every reader and writer moved to
config + disk (desired) or the snapshot (observed). Nothing in it was
non-derivable, so nothing is migrated and an existing file is left
unread. The web-side PluginStateManager (state_manager.py) is removed;
the display's plugin_state.PluginStateManager is the only state machine.
- StateReconciliation compares config + disk with the snapshot, reporting
enabled-but-not-loaded and older-version-loaded as no_action findings.
- Backups list installed manifests with enabled from config.json.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The web process built its own PluginManager and loaded plugins into itself:
store installs and updates loaded or reloaded a web-side copy, and config
saves and enable/disable called on_config_change, on_enable and on_disable
on it. None of that reached the panel, and /plugins/installed reported
runtime state from those copies.
- Add PluginCatalog (src/plugin_system/plugin_catalog.py): manifests,
directories, display modes, installed version, schema and config reads,
with no way to run a plugin. app.py and both blueprints use it; the
plugin_manager blueprint attribute is gone.
- Remove every lifecycle call from the web routes. Config changes already
reach the display through ConfigService (on_config_change) and the
enabled-set reconcile.
- Health and metrics readers move to api_v3.health_tracker /
resource_monitor. /plugins/installed reports loaded/state/error_info as
null (the display does not publish them) and enabled by the display's
rule.
- Store install, update and uninstall answer restart_required when the
running display will not pick the change up by itself
(display_restart_required). The restart banner follows the flag via
window.noteRestartRequired instead of the /config/main URL heuristic;
/config/main now sends restart_required: true.
- The one remaining in-process import of plugin code (Starlark helper
modules, oauth_flow action scripts) goes through
_import_plugin_code_in_web_process() until a web-entry contract.
- /plugins/installed reports vegas_participation (from #682) from the
user's setting or the manifest, with vegas_participation_source; when
only the plugin's code decides it, null with source 'runtime', since the
web process no longer has plugin instances to ask.
- Check & Update All keeps its restart flags when the final list refresh
fails, and asks for a restart when an enabled plugin's first request got
no answer and the re-sent one found it up to date.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three new hardware-free modules holding code the scoreboard plugins carry
as identical copies (executable AST, docstrings stripped, checked across
every carrying plugin at ledmatrix-plugins 30455671). The bodies are the
plugins'; the changes are type annotations for the mypy ratchet, the
colour helpers losing their leading underscore as public free functions,
and two comments that described the plugins' files.
- src/common/sports_celebration.py: SportsCelebrationMixin, the score/win
takeover drawn by afl, football, hockey, nrl and soccer
(_draw_celebration_layout and the palette, backdrop, scenery, confetti,
crest and _fit_font steps, with their class constants), plus the colour
helpers (logo_palette, lift_color, cap_luminance, mix_color, ...). Only
the drawing: _start_celebration, _check_for_goal/_check_for_score,
_check_for_win and display() differ between the plugins and stay there.
- src/common/sports_fetch.py: SportsFetchMixin, the four SportsCore methods
identical in all nine scoreboards: _fetch_season_directly,
_background_fetches_espn_ranges, _needs_previous_day and
_wants_live_odds, with _LOOKBACK_CUTOFF_HOUR and _LIVE_ODDS_LOOKAHEAD.
_get_timezone, _extract_game_details and _fetch_data are as identical
and stay behind, for the reasons sports_shared gives (a per-plugin
import; the abstract contract); so does SportsUpcoming.__init__, since
no src/common mixin has a constructor.
- src/common/sports_card_wrappers.py: SportsCardWrappersMixin, the
seventeen sports_card delegations the eight game renderers carry (15 in
all eight, 2 in all but football, whose own versions override them).
_schema_font_size/_resolve_font_size look identical but read each
plugin's own _SCHEMA_PATH, so they stay.
Each mixin has no __init__ and creates no attributes (the host contract is
declared as annotations only), defines no name the mixins beside it
define, and documents the attributes it reads; a host-contract test
parses each and fails on an undocumented read. A method kept on a
plugin's class wins over the mixin's.
Tests: behaviour ported from the plugins' celebration, odds, lookback and
date-range tests against stub hosts carrying exactly the contract, with
crests drawn by the test (test_sports_celebration.py, test_sports_fetch.py,
test_sports_card_wrappers.py), and test_sports_stage3_parity.py, which with
LEDMATRIX_PLUGINS set compares every body with every plugin copy that is
left (58 pass against the plugins today; a copy that is gone counts as
adopted). All three modules are on the mypy ratchet, in
src/common/README.md, the CHANGELOG's Unreleased section and
SPORTS_UNIFICATION's module table. Nothing in core uses them yet.
Full suite: the same 67 failing test ids as main (Windows-only), 77 more
passing.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(common): favorite_team_check and sports_timezone, promoted from the scoreboards (sports consolidation stage 2)
Two new hardware-free modules, taken from files the scoreboard plugins carry
as copies:
- src/common/favorite_team_check.py: FavoriteTeamCheck(logger, leagues), the
seven byte-identical <sport>_favorite_check.py copies. Same code; the only
additions are two type annotations (for the mypy ratchet).
- src/common/sports_timezone.py: resolve_timezone_name(), resolve_timezone(),
system_timezone_name(), from the ten <sport>_timezone.py copies. They
differed only in the plugin label named in the nothing-resolved warning and
the write-back-bug values, which become keyword-only arguments
(plugin_label, writeback_fixed_in). Same resolution order and log text.
Tests are ported from the plugins' own (test_favorite_check.py,
test_schedule_note_uses_game_dates.py, test_timezone_resolution.py; the
timezone ones run once per plugin's values and pin the exact warning text).
Both modules are on the mypy ratchet, in src/common/README.md, the CHANGELOG's
Unreleased section and SPORTS_UNIFICATION's module table. Nothing in core uses
them yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(common): bdf_font and json_body shipped in 3.5.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(common): annotate the favourite check's deliberate except/pass for Bandit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* ci: mypy ratchet -- keep type-clean modules clean
mypy-clean.txt lists the 71 modules under src/ that type-check clean;
scripts/check_types.py runs mypy (--follow-imports=silent) on exactly
those files and fails on any error or a missing/unsorted/duplicate entry.
A new "Type check (mypy ratchet)" CI job runs it with mypy 1.20.2 and
pinned stubs; the manual pre-commit mypy hook now runs the same script
(a local hook, so mypy sees the installed requirements like CI does).
35 modules were made clean with annotation-only fixes: hints, typing.cast,
TYPE_CHECKING imports, implicit-Optional defaults made explicit, and
annotations widened (never guards removed) where mypy called a defensive
isinstance check unreachable. No runtime behaviour change.
mypy.ini: numpy and orjson are treated as Any (follow_imports=skip, also
for stubs). numpy 2.3+ stubs use 3.12 `type` statements that mypy won't
parse at python_version 3.10, and orjson is optional, so seeing its stubs
made the result depend on whether it was installed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore: annotate check_types.py's list-form mypy subprocess
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>