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.
This commit is contained in:
ChuckBuilds
2026-09-04 23:20:26 -04:00
parent 91d15a8943
commit 48a004a0ac
3 changed files with 29 additions and 13 deletions
+2 -1
View File
@@ -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"
@@ -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()
+18 -10
View File
@@ -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)