From 48a004a0ac8af0e9f7f098c033487d5e3070edb3 Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Fri, 4 Sep 2026 20:18:23 -0400 Subject: [PATCH] fix(core): register tom_thumb, accept frame_hold in the test double, wire api_v3's managers Three independent fixes found while validating every plugin on a 256x64 rig. FontManager never registered tom_thumb even though assets/fonts/tom-thumb.bdf ships with the core, so every plugin offering it logged "Font family 'tom_thumb' not found" (16 warnings per countdown render) and had to carry a private loader to use a bundled font. Closes #524. VisualTestDisplayManager.set_scrolling_state() lacked the frame_hold parameter that DisplayManager gained, so any plugin passing it died with TypeError at render time and failed every size. Nine plugins now make that call; ledmatrix-stocks and ledmatrix-leaderboard were failing outright and the other seven only passed because their scroll path was unreachable without data. Closes #525. api_v3 declared module-level config_manager/plugin_manager = None that nothing ever assigned -- app.py sets the blueprint attributes, which the other 150+ call sites use. Three sites read the decoys, so /health reported the config unreadable and the plugin system uninitialised (making "degraded" permanent and unreachable-by-design) and /display/current fell back to a hardcoded 128x64 on every rig. The decoys are removed rather than assigned, so a bare name is now a NameError at test time instead of a silent None. The same function's first-call uptime was computed from two separate clock reads and came out negative. Closes #529. Verified on the rig: both previously-failing plugins render, the tom_thumb warnings are gone, /health reports "healthy" with all three checks passing, and /display/current reports the real 256x64. --- src/font_manager.py | 3 +- .../testing/visual_display_manager.py | 11 ++++++-- web_interface/blueprints/api_v3.py | 28 ++++++++++++------- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/font_manager.py b/src/font_manager.py index 8be283be..93640808 100644 --- a/src/font_manager.py +++ b/src/font_manager.py @@ -96,7 +96,8 @@ class FontManager: self.common_fonts = { "press_start": "assets/fonts/PressStart2P-Regular.ttf", "four_by_six": "assets/fonts/4x6-font.ttf", - "five_by_seven": "assets/fonts/5x7.bdf" + "five_by_seven": "assets/fonts/5x7.bdf", + "tom_thumb": "assets/fonts/tom-thumb.bdf" # Note: cozette_bdf removed - font file not available # To re-enable: download cozette.bdf from https://github.com/the-moonwitch/Cozette # and add: "cozette_bdf": "assets/fonts/cozette.bdf" diff --git a/src/plugin_system/testing/visual_display_manager.py b/src/plugin_system/testing/visual_display_manager.py index e33d2309..732d6d61 100644 --- a/src/plugin_system/testing/visual_display_manager.py +++ b/src/plugin_system/testing/visual_display_manager.py @@ -506,9 +506,16 @@ class VisualTestDisplayManager: # Scrolling state (no-op interface compat) # ------------------------------------------------------------------ - def set_scrolling_state(self, is_scrolling: bool): - """Set the current scrolling state (no-op for testing).""" + def set_scrolling_state(self, is_scrolling: bool, frame_hold: int = 1): + """Set the current scrolling state (no-op for testing). + + ``frame_hold`` mirrors DisplayManager.set_scrolling_state so a plugin + that paces its scroll can be rendered here. Without it every such + plugin raised TypeError at render time and failed every size, which is + invisible until a plugin happens to pass the argument. + """ self._scrolling_state['is_scrolling'] = is_scrolling + self._scrolling_state['frame_hold'] = frame_hold if is_scrolling: self._scrolling_state['last_scroll_activity'] = time.time() diff --git a/web_interface/blueprints/api_v3.py b/web_interface/blueprints/api_v3.py index 35dfd885..845c6884 100644 --- a/web_interface/blueprints/api_v3.py +++ b/web_interface/blueprints/api_v3.py @@ -78,8 +78,11 @@ def _scrub_git_remote_url(url: str) -> str: return url # Will be initialized when blueprint is registered -config_manager = None -plugin_manager = None +# NOTE: the managers live on the blueprint object (app.py sets +# api_v3.config_manager / api_v3.plugin_manager). Deliberately not +# mirrored as module globals: a bare `config_manager` used to resolve to +# a None that was never assigned, which silently disabled the /health +# checks and made /display/current fall back to a hardcoded 128x64. plugin_store_manager = None saved_repositories_manager = None cache_manager = None @@ -1598,11 +1601,16 @@ def get_health(): } # Check web interface service + # Stamp the start time before measuring against it -- reading it with a + # fallback of time.time() and only assigning afterwards made the very + # first call subtract two separate clock reads, reporting a small + # negative uptime. + if not hasattr(get_health, '_start_time'): + get_health._start_time = time.time() health_status['services']['web_interface'] = { 'status': 'running', - 'uptime_seconds': time.time() - (getattr(get_health, '_start_time', time.time())) + 'uptime_seconds': time.time() - get_health._start_time } - get_health._start_time = getattr(get_health, '_start_time', time.time()) # Check display service display_service_status = _get_display_service_status() @@ -1613,8 +1621,8 @@ def get_health(): # Check config file accessibility try: - if config_manager: - test_config = config_manager.load_config() + if api_v3.config_manager: + test_config = api_v3.config_manager.load_config() health_status['checks']['config_file'] = { 'status': 'accessible', 'readable': True @@ -1633,9 +1641,9 @@ def get_health(): # Check plugin system try: - if plugin_manager: + if api_v3.plugin_manager: # Try to discover plugins (lightweight check) - plugin_count = len(plugin_manager.get_available_plugins()) if hasattr(plugin_manager, 'get_available_plugins') else 0 + plugin_count = len(api_v3.plugin_manager.get_available_plugins()) if hasattr(api_v3.plugin_manager, 'get_available_plugins') else 0 health_status['checks']['plugin_system'] = { 'status': 'operational', 'plugin_count': plugin_count @@ -2468,8 +2476,8 @@ def get_display_current(): # Get display dimensions from config try: - if config_manager: - main_config = config_manager.load_config() + if api_v3.config_manager: + main_config = api_v3.config_manager.load_config() hardware_config = main_config.get('display', {}).get('hardware', {}) cols = hardware_config.get('cols', 64) chain_length = hardware_config.get('chain_length', 2)