Give plugins access to device-wide config, and a global scroll frame rate - #424
Conversation
…rate
The sports scoreboards read `getattr(self, 'global_config', {})` to find a
shared scroll frame rate, but nothing ever set that attribute: the loader
constructs plugins with only plugin_id/config/display_manager/cache_manager/
plugin_manager (plugin_loader.py:671), `global_config` appears nowhere in
src/, no plugin manager assigns it, and BasePlugin has no __getattr__ to
synthesize it. The lookup always returned {}, so the ten scroll_display.py
copies that thread target_fps through to ScrollHelper could never fire on any
core. There was also no global target_fps to find -- the only one in the
template is display.vegas_scroll.target_fps, which is Vegas-scoped.
Adds the missing half:
- `BasePlugin.global_config` resolves the full config via
plugin_manager.config_manager, then cache_manager.config_manager, then {}.
Same order the sports timezone helpers already use. Exceptions are swallowed
to debug so an unreadable config can never stop a plugin loading, and a
non-dict result is rejected rather than handed to callers that will .get()
it and feed the result to numeric code.
- A top-level `target_fps` (default 100), exposed on the General tab and
validated 30-200 on save to match ScrollHelper.set_target_fps -- which
clamps silently, so a rejected save reports a value that would otherwise
appear to save and then behave differently.
The property has a setter deliberately. news, stock-news, ledmatrix-stocks,
ledmatrix-elections, ledmatrix-leaderboard and nfl-draft all assign
`self.global_config = config.get('global', {})`; without a setter that raises
"property has no setter" and those six plugins stop loading. Reproduced, then
pinned with a test.
target_fps is also kept out of the `is_general_update` key list: that branch
treats a missing web_display_autostart as an unchecked box, so counting a
target_fps-only POST as a General save would silently switch autostart off.
Verified end to end: config.json -> BasePlugin.global_config ->
scroll_display's existing block -> ScrollHelper.target_fps 120 -> 100, with no
plugin-side change needed. Suite 1441 passed; the 4 failures
(test_display_dirty_tracking, test_web_api::test_get_system_status, two in
test_state_reconciliation) are pre-existing and reproduce identically on a
clean tree.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
|
Warning Review limit reached
Next review available in: 41 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds device-wide ChangesTarget FPS configuration
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant GeneralForm
participant save_main_config
participant RootConfiguration
GeneralForm->>save_main_config: Submit target_fps
save_main_config->>save_main_config: Validate integer from 30 through 200
save_main_config->>RootConfiguration: Store target_fps
RootConfiguration-->>save_main_config: Persisted configuration
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
target_fps had no copy to converge -- self.config is only the plugin's own
slice, so a device-wide value simply wasn't reachable, which is why the
scoreboards' getattr(self, 'global_config', ...) always saw {}.
Documents the core-side property added in ChuckBuilds/LEDMatrix#424: the
resolution order, reading it as getattr(...) so plugins still load on older
cores, that it is read-only (mutating the live config has bitten this repo
before), and that assignment still overrides it -- which news, stock-news,
ledmatrix-stocks, ledmatrix-elections, ledmatrix-leaderboard and nfl-draft
depend on.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 9 |
| Duplication | 0 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/plugin_system/base_plugin.py`:
- Around line 202-203: Update the plugin-manager configuration selection in the
surrounding method to accept any dictionary, including an empty one, so it
retains priority over cache_manager.config_manager; replace the truthiness
requirement while preserving the non-dictionary fallback behavior. Add a
regression test covering an empty plugin-manager configuration alongside a
populated cache-manager configuration and assert the empty dictionary is
returned.
In `@test/test_plugin_system.py`:
- Around line 354-359: Update test_template_ships_a_global_target_fps to assert
that the shipped config’s target_fps value equals 100, rather than only checking
that it is an integer.
In `@web_interface/blueprints/api_v3.py`:
- Around line 755-768: Update the target_fps validation block to reject JSON
floats and booleans before calling int(), while preserving acceptance of valid
integer values and the existing documented 400 error response. Add an API
regression test covering target_fps=90.5 and verify it returns the
integer-validation error without updating current_config.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: d3ae3d27-931f-4890-8ead-60377f4c543e
📒 Files selected for processing (6)
config/config.template.jsonsrc/plugin_system/base_plugin.pytest/test_plugin_system.pytest/test_web_api.pyweb_interface/blueprints/api_v3.pyweb_interface/templates/v3/partials/general.html
- Reject floats and bools before int() in the target_fps save path. A JSON
body can carry them, where int(90.5) silently stored 90 and true stored 1.
Form posts send strings, so '90.5' already failed in int().
- Assert the template's target_fps is 100, not merely an int, so the
documented default is actually pinned.
- Empty-config precedence: keeping the `and config` check deliberately, now
spelled out in the comment and covered by a test. Both managers default to
the same config/config.json, so falling through cannot pick up a different
file's settings; treating {} as an answer would instead return {} when the
first manager simply hasn't loaded yet, silently disabling every setting
read through the property -- the failure this property exists to fix.
Suite 1446 passed. The float-rejection test was checked to fail without the
guard.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
#425 was squash-merged, so #426's base content now lives in main under a different SHA. Reconciled the three resulting conflicts: - .github/workflows/test.yml: kept #426's superset test list (its unification suites plus test_element_style.py); main had only element_style from #425. - CHANGELOG.md: kept #426's version, which promotes main's "Unreleased" section into the "3.2.0" release and is a verified superset of main's content (no lines dropped). - test_sports_base_characterization.py: kept #426's side for all three blocks. Two are semantic, not cosmetic: #425 pinned the pre-fix behaviour (hockey/baseball events dropped -> *_returns_none), while #426 carries the actual fixes (2486bdb, 2eea7a7) and updated the tests to *_still_extracts. Taking main's side would fail against #426's fixed code. The third is the get_background_service import path, which is .core in #426 after the package split. Also refreshed a stale comment that still named the removed test. #424's global_config plumbing merges in cleanly; verified the interlock end to end on the merged tree (global_config target_fps -> SportsScrollDisplay 90.0). 435 tests pass across the affected suites; the lone failure (test_get_system_status) is one of the four pre-existing on main. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
… plugin fleet (#239) * chore: remove dead files with zero importers hockey-scoreboard/base_classes.py and scoreboard_renderer.py are stale near-duplicates of hockey.py's live code; basketball_helpers.py is an unused font-loading helper. Nothing imports any of them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4 * perf: cache per-render font loads in of-the-day, web-ui-info, on-air - of-the-day: the classic-fallback fonts (used when the core lacks src.element_style) were reloaded from disk on every render; they are config-independent, so load once and reuse. - web-ui-info: display() reloaded the 4x6 font on every call. - on-air: the shrink-to-fit path reloaded the scaled TTF each frame for wide labels; now memoized by (path, size). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4 * docs: shared sports code lineage map, scroll-key semantics, 8-size harness matrix - New 08-shared-sports-code.md: the three scoreboard lineages, per-module drift table, guarded convergence pattern, sunset rule for local copies, and the fix-all-lineage-members-in-one-PR rule (commit 8d33894 as the cautionary example). - 03-advanced-features.md: document the three incompatible scroll_speed unit semantics (px/frame, px/second, inverted frames-per-step divisor). - 07-testing-ci-and-registry.md: the harness default matrix is 8 sizes, not 4. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4 * feat: standardize font/size/color customization and x-advanced scroll keys Accessibility rollout, additive only — default rendering is byte-identical in every plugin (custom faces load only when config differs from the schema default, so web-UI merged-defaults configs are unaffected): - clock-simple: honor the customization font/font_size keys the schema already declared (code previously read only text_color); existing golden images pass unchanged. - news: per-element headline_text/source_text font+size; source line color was previously hardcoded (150,150,150) and is now configurable. - mqtt-notifications: message_text font face+size (single text style). - countdown: name_font_family — the name line previously had per-element size/color but shared the value line's font face. - tide-display: tide_text/label_text font+size+color, routed by the existing palette constants; chart colors untouched. - youtube-stats: channel_name/subscriber_count/view_count font+size+ color (plugin previously had zero styling config). - x-advanced added to scroll_speed/scroll_delay fine-tuning keys in nrl, ufc, elections, stocks, leaderboard, news, odds-ticker, march-madness, text-display schemas (UI hint only). text-display and on-air deliberately unchanged: their existing config surface already covers font face, size, and colors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4 * feat: honor global target_fps in elections, stocks, nfl-draft, march-madness, text-display Non-scoreboard half of the global-FPS rollout (canonical pattern from ledmatrix-leaderboard): read global_config target_fps/scroll_target_fps, probe set_target_fps with hasattr, clamp 30-200 with a frame_time_target fallback for older ScrollHelper builds. - elections, stocks: adopt the block (previously ignored the global FPS). - nfl-draft: also set an explicit scroll_delay (pacing was previously left at the helper default and unconfigured). - march-madness: plugin-level display_options.target_fps still wins, falls back to the global; added the missing older-core fallback branch. - text-display: added the hasattr guard + fallback (its 240 ceiling was already clamped to 200 inside the core helper). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4 * test: harness fixtures for 8 plugins, goldens for 6; fix of-the-day 64px overflow Deterministic test/harness.json for countdown, christmas-countdown, 7-segment-clock, text-display, static-image, web-ui-info, of-the-day, on-air (config + freeze_time archetype; static-image renders a bundled test-pattern PNG). Golden images committed for the six deterministic renderers — every golden was generated twice and byte-compared before committing. No goldens for web-ui-info (renders the host IP) or on-air (active state is event-driven). The of-the-day fixture immediately exposed a real bug: on 64px-wide panels the title (PressStart2P@8) and its underline drew past the right edge, and body lines ran past the bottom on 64x32. Titles now ellipsize to the panel width, the underline is clamped inside the panel, and body lines stop before the bottom edge. Wider panels render identically. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4 * chore: bump versions for all plugins changed so far Sports scoreboards still receiving font-cache/odds/fps work (afl, baseball, football, soccer, lacrosse, f1) will be bumped with those changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4 * fix: address review findings on FPS sourcing, panel clamps, and fixtures - elections/stocks/nfl-draft/march-madness: source the FPS target from the plugin config's 'global' section (the news/leaderboard convention) instead of a never-set attribute; elections also refreshes it in on_config_change. - text-display: single _apply_target_fps helper used by init and on_config_change — the latter previously called set_target_fps unconditionally and would raise AttributeError on older cores; the stored value is now the effective 30-200 clamped rate. - of-the-day: clamp title_x after the user layout offset; guard _fit_title against a panel too narrow for the ellipsis itself. - clock-simple: clamp the combined time+AM/PM block to start on-panel when a user-selected font exceeds the width. - youtube-stats: truncate the channel name by measured width and derive row height from the selected fonts when a custom font is set (identical layout at the monospace default); validate color length in _rgb. - tide-display: same color-length validation; unfold the one-line try/except flagged by Ruff E701. - countdown fixture: pin font/color schema defaults explicitly so a future default change cannot silently invalidate the goldens. All affected plugins re-verified with the harness; clock-simple, countdown, and of-the-day goldens pass unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4 * fix(text-display): resolve font_path against the core install, not just cwd CI runs the safety harness from the workspace root rather than the core checkout, so the plugin's relative font_path missed assets/fonts and fell back to PIL's default font — mismatching the committed goldens (generated with the real font). Add a resolution strategy that walks up from the display manager's module location to find the core's assets, making font loading cwd-independent. Goldens now pass from both working directories. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4 * perf(football-scoreboard): cache the record font instead of reloading per frame The scorebug draw paths (SportsUpcoming/SportsRecent in sports.py, FootballLive in football.py) reloaded 4x6-font.ttf from disk on every rendered frame when records/rankings are shown; game_renderer's _draw_records_or_rankings did the same unconditionally. The face is now cached once in _load_fonts (fonts['record']) with a lazy memo in the renderer, using the accessor pattern ufc-scoreboard already ships. Rendering is pixel-identical: the full adaptive-layout and score- celebration golden suites (46 tests) pass unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4 * perf(hockey,lacrosse): cache record/shots fonts instead of reloading per frame Same treatment as football-scoreboard: fonts['record'] and fonts['shots'] cached in _load_fonts, scorebug draw sites use the accessor pattern, and game_renderer's fallback disk load is memoized (its detail-font primary lookup — a real lineage difference from football — is preserved). Also fixes a latent crash: hockey.py's shots font load had no try/except, so a missing 4x6-font.ttf killed the live render instead of degrading. Harness output is byte-identical to the pre-change baselines for both plugins; offline test scripts (favorite-live-boost, non-favorite-live- duration, timezone-resolution) all pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4 * chore: bump football (2.10.0) and lacrosse (1.6.0) for the font-caching change Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4 * perf(afl,baseball,soccer): cache record/ranking font instead of reloading per frame Completes the font-caching sweep started for football/hockey/lacrosse (cf93709, 640c081). Same treatment across the three remaining sports-scoreboard lineages: - afl-scoreboard: sports.py had three separate uncached reload sites (SportsUpcoming/SportsRecent ranking overlays); game_renderer.py was already caching fonts["record"] in _load_fonts. Added the same cache to sports.py's _load_fonts and switched all three sites to the self.fonts.get("record") accessor. - baseball-scoreboard: sports.py had two uncached reload sites; fixed the same way. game_renderer.py already reused fonts['detail'], no change needed there. - soccer-scoreboard: sports.py had three uncached reload sites, and game_renderer.py's _draw_records_or_rankings reloaded unconditionally on every call (no fonts-dict entry at all) -- given the football game_renderer lazy-memo treatment (getattr(self, '_record_font', ...)). f1-scoreboard checked and needs no change: its renderer loads all fonts once in __init__ (f1_renderer.py:180), no per-frame reload pattern exists there. Verified pixel-identical: rendered afl/baseball/soccer live+recent+ upcoming at 128x64 with show_records and show_ranking forced on, before and after the change (via git stash) -- byte-for-byte identical PNGs in all cases. Full safety harness (all 8 sizes) passes for all three plugins with no new warnings. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ * perf(basketball,nrl,ufc,baseball): finish the record-font caching sweep Completes the lineage-wide font-caching change (football, hockey, lacrosse, afl, soccer landed earlier): - basketball: record + tournament-date fonts cached in _load_fonts; game_renderer memoizes its record font. - nrl: record font cached; game_renderer memoized. - ufc: upcoming/recent scorebug paths use the cached record font the plugin already loaded for its live layout. - baseball: (name,size)-keyed memo in _load_custom_font_from_element_config plus a BDF native-size cache, collapsing the per-frame 10-rung font ladder walks in the traditional-scoreboard and at-bat screens into dict lookups; the record-font block moves after the config fallback so it is set on every path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4 * refactor(sports): prefer the core-shipped odds manager with a bundled fallback The eight non-UFC scoreboards imported their local base_odds_manager copy unconditionally. They now try src.base_odds_manager first (a functional superset: adds cache_ttl support) and fall back to the bundled copy on cores that don't ship it. Both branches are module-level, so they stay collision-safe under the loader's bare-name isolation. In afl/nrl/soccer/basketball manager.py the odds import sat inside the combined BasePlugin guard, so a missing odds module would have nulled BasePlugin (and in nrl's case NameError'd — its except branch never set BaseOddsManager). It now has its own nested guard. UFC keeps its local copy unconditionally: it is a genuine MMA fork (athlete odds), not a drifted duplicate. Local copies stay bundled per the sunset rule in docs/plugin-development/08-shared-sports-code.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4 * fix(countdown): load family fonts cwd-independently so CI goldens match The safety harness on CI runs from the workspace root, not the core checkout, so the core FontManager's cwd-relative assets/fonts scan came up empty and resolve_font silently degraded to PIL's default face — drifting every committed golden (the named failure in the last two safety runs). The type of the degraded font gives no signal (current Pillow's load_default() is itself a FreeTypeFont), so the miss is detected via the manager's font catalog, and the family's real file is then resolved against the core install the display manager was loaded from (same strategy text-display already uses). Verified: goldens pass from both the core root and a foreign cwd. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4 * feat(sports): honor the global target_fps in every scoreboard's scroll mode The ten scroll_display.py files paced frames only from scroll_delay, so the global smooth-scrolling FPS setting (target_fps / scroll_target_fps) silently never reached the sports scoreboards. ScrollDisplay and ScrollDisplayManager now accept a trailing global_config kwarg (threaded from every manager construction site), and _configure_scroll_helper applies the canonical set_target_fps block with the clamped frame_time_target fallback for older cores. When no global value is set, pacing is unchanged. Also removes the dead _get_target_fps helpers in afl/nrl/soccer whose value was computed but never applied, and bumps f1 to 1.8.0 (its only change in this PR). Verified: harness green on all ten plugins (f1's committed goldens pass unchanged); football adaptive/celebration and soccer celebration pytest suites green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4 * fix: address review findings on measurement fallback and registry dates - tide-display: when display-manager measurement fails, fall back to the drawing font's own getbbox metrics before the 4px/char estimate, so a custom face never draws wider than the width reported. - text-display, countdown: log module-relative font-probe failures at debug instead of swallowing them silently. - afl/baseball manifests carried a stale last_updated (2026-07-17) that the registry echoed; set to 2026-07-31 and teach update_registry.py to sync last_updated even when latest_version is unchanged, so catalog timestamps can no longer drift from manifests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4 * ci: don't echo unvalidated plugin ids in the harness failure summary Plugin ids are derived from PR file paths, so in the invalid-id branch the raw string is fork-controllable; echoing it into workflow commands is needless exposure. Redact it and record a placeholder in the summary instead — valid ids (the useful signal) are unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4 * fix(ufc): enable the scoreboard managers; test: mock fixtures for six scoreboards The UFC managers pass sport_key='ufc_scoreboard' and SportsCore looked up config under f'{sport_key}_scoreboard' — the nonexistent 'ufc_scoreboard_scoreboard' — so mode_config was always empty, is_enabled always False, and every UFC screen rendered blank. The lookup now uses the key the adapter actually writes ('ufc_scoreboard'). Found while building harness fixtures: no cache/mock/config contents could make the managers render. Adds deterministic harness fixtures (config + cached-schedule mock data + frozen clock) for baseball, basketball, football, hockey, lacrosse, and soccer, so the safety harness renders real game cards for recent/upcoming (and live where the plugin's live path reads the cache: basketball, soccer, plus baseball via its test_mode passthrough) instead of blank no-data screens. Where the live path is a direct network fetch with no cache read (hockey, football, lacrosse), live is disabled in the fixture with the reason documented in each harness.json. Verified: all sizes PASS and two consecutive runs are byte-identical for every plugin. UFC gets no fixture yet: its fighter-headshot loader has no negative caching and no config toggle, so offline runs spend ~15s of retries per headshot per render — needs a small plugin change first (follow-up). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4 * fix: address CodeRabbit review findings across scoreboards, tide, countdown - nrl manager: remove a stray unconditional BaseOddsManager = None that overwrote a successful bundled-fallback import on older cores (critical review catch). - soccer + afl sports.py: move the cached record-font block after the config/fallback branches so it is set on every path, matching the other seven copies. - hockey manager: drop the duplicate ScrollDisplayManager construction; the first instance (which enable_scrolling reads) now persists instead of being silently replaced. - all ten scroll_display.py: coerce target_fps to float before comparing so a malformed global config degrades to scroll_delay pacing instead of raising in __init__. - tide-display: resolve custom fonts against the core install (cwd- independent, same strategy as text-display/countdown) and clamp the three label positions that could go negative or overflow with large custom fonts. - baseball sports.py: narrow the BDF strike-size retry to OSError and cache the fallback-default font under the requested key so a misconfigured font stops hitting the disk per frame. - countdown: log per-candidate font-load failures instead of silently continuing, and memoize the FontManager catalog-miss check that was statting the filesystem on every render. The bare-name fallback module rename suggestion is deliberately not applied — module-level bare imports are collision-safe under the core loader's isolation rules (see the review reply and 08-shared-sports-code.md). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4 * docs: record how plugins read device-wide settings target_fps had no copy to converge -- self.config is only the plugin's own slice, so a device-wide value simply wasn't reachable, which is why the scoreboards' getattr(self, 'global_config', ...) always saw {}. Documents the core-side property added in ChuckBuilds/LEDMatrix#424: the resolution order, reading it as getattr(...) so plugins still load on older cores, that it is read-only (mutating the live config has bitten this repo before), and that assignment still overrides it -- which news, stock-news, ledmatrix-stocks, ledmatrix-elections, ledmatrix-leaderboard and nfl-draft depend on. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ * fix: address CodeRabbit round-2 findings (import narrowing, docs, probe logging) - The odds-manager convergence guards now catch ModuleNotFoundError and check exc.name, so only an absent core module triggers the bundled fallback; an import failure from inside src.base_odds_manager (missing dependency) surfaces instead of being masked. Applied to all eight sports.py guards and the four manager.py nested guards; verified both branches with meta-path simulations. - 08-shared-sports-code.md: the global_config example now uses the getattr form the same section mandates. - tide-display: the module-relative font probe logs failures at debug level instead of a blind except/pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4 * chore: reclassify the scoreboard releases as PATCH CONTRIBUTING.md scopes MINOR to new features and schema additions. None of these ten plugins gained either: the changes are font-load caching, the odds-manager import switch, target_fps threading, and test fixtures. The only schema edits (nrl, ufc) add `x-advanced` UI hints, not options. That makes them PATCH. Each new version is set one patch above **main's** current version rather than above the branch's, which also fixes a collision: soccer had been bumped to 2.5.0 here while main independently released its own 2.5.0, so two different sets of changes shared a version number. Now 2.5.1. plugins.json could not simply be regenerated -- update_registry.py refuses to lower a version, so it skipped all ten and left the registry advertising the old MINOR numbers. Reset the generated file to main's state and regenerated from there, which the tool accepts as an increase. Verified afterwards that every latest_version moves up and none moves down. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ * fix(clock-simple): stop an oversized date clipping at both ends The date and weekday draws passed no x, so draw_text auto-centred them with (width - text_width) // 2 and no clamp. _fit_date returns its shortest candidate even when that still overflows -- reachable now that the font is user-configurable -- and a negative x clips the string at *both* ends, losing the leading characters rather than just the tail. Adds _centered_x, which clamps to 0 so overflow clips on the right only. It returns None when measuring raises, deferring to draw_text's own centring rather than guessing, mirroring _text_fits assuming a fit so a measurement failure never hides content. The time and AM/PM paths already clamp (max(0, ...) on time_x, and max(0, min(...)) on ampm_x), so this closes the remaining case. Defaults are unchanged: all three committed goldens still match byte for byte, harness green at all 8 sizes. Verified the fix directly -- "Aug 1st" at 84px on a 64px panel gives draw_text x=-10 vs _centered_x x=0. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ * docs: add the validated scroll-display adoption recipe Core 3.2.0 ships src/common/sports_scroll.py, the orchestration half of scroll_display.py. The content half (prepare_scroll_content, _load_separator_icons) stays per-plugin permanently -- a survey of the eight copies that share a shape found eight distinct bodies, because each draws its own game card. Same method name, different job. Documents the mechanical adoption once a plugin floors at 3.2.0, the byte-comparison acceptance gate, and the two gotchas the hockey pilot surfaced (the inherited os.path use in _load_separator_icons, and the now-dead scroll_helper guards). Measured on hockey-scoreboard: 691 -> 289 lines, all 16 harness renders byte-for-byte identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4 * fix: sync f1 release metadata, correct the doc's import example Review follow-ups on #239. f1-scoreboard's manifest disagreed with itself: last_updated said 2026-07-28 while its newest versions[] entry for the same 1.7.1 said 2026-08-01. update_registry.py mirrors last_updated into the catalog, so the stale date propagated there too. Set it from the release entry and regenerated plugins.json. The 08-shared-sports-code doc still showed `except ImportError` for the base_odds_manager guard, but the code was narrowed to ModuleNotFoundError with a name check in an earlier round -- so the doc was teaching the pattern its own examples no longer use. A bare ImportError also swallows failures raised *inside* a core module that is present, silently loading the bundled copy and hiding a broken install. clock-simple's font loader caught bare Exception; narrowed to OSError, which is what FreeType raises for a missing, unreadable or malformed face. All three committed goldens pass unchanged. Left alone: - The measurement fallback in clock-simple stays broad on purpose. It runs on the render path and deliberately degrades to draw_text's own centring; letting a measurement hiccup propagate would blank a clock. - soccer-scoreboard's _check_favorite_teams blocking update() is real, but it arrived in #233 (49b0fe2) from main and is not this PR's code. - Five other manifests carry the same date drift from earlier PRs; out of scope here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FgbA8SMutQQpXkMG8LMmC4 --------- Co-authored-by: Claude <noreply@anthropic.com>
Problem
The sports scoreboards in
ledmatrix-pluginsread a shared scroll frame rate like this:Nothing ever sets that attribute. The loader constructs plugins with only
plugin_id,config,display_manager,cache_manager,plugin_manager(plugin_loader.py:671);global_configappears zero times in all ofsrc/; no plugin manager assigns it; andBasePluginhas no__getattr__to synthesize it. So the lookup always evaluates to{}.The consequence: ten
scroll_display.pycopies threadtarget_fpsthrough toScrollHelper, and none of it can ever fire on any core. The plumbing is complete and correct on the plugin side, waiting on a source that doesn't exist.There was also nothing to find even if it were delivered — the only
target_fpsin the template isdisplay.vegas_scroll.target_fps, which is Vegas-scoped. A device-wide setting did not exist.Worth noting for reviewers: I first tried to confirm this from a running rig's logs, but
ScrollHelper.set_target_fpslogs at debug, so absent log lines prove nothing. The conclusion rests on the static chain above.What this adds
BasePlugin.global_config— resolves the full config viaplugin_manager.config_manager, thencache_manager.config_manager, then{}. That order mirrors the timezone helpers the sports plugins already ship (8d33894).Two deliberate hardening choices:
dictresult is rejected. Callers.get()this and feed the result into numeric code, so handing back whatever a stub or half-built manager returned would fail later and further from the cause.A top-level
target_fps(default 100), on the General tab, validated 30–200 on save. Those bounds matchScrollHelper.set_target_fps, which clamps silently — rejecting at the API means a value that would have been quietly altered gets reported, instead of appearing to save and then behaving differently.The setter is not incidental
A read-only property would have broken six shipped plugins.
news,stock-news,ledmatrix-stocks,ledmatrix-elections,ledmatrix-leaderboardandnfl-draftall do:Against a getter-only property that raises
property 'global_config' of 'NewsTickerPlugin' object has no setterand those plugins stop loading. I reproduced it, then pinned it with a test. Assignment now wins over the resolved value, preserving their existing behaviour exactly.Similarly,
target_fpsis deliberately kept out of theis_general_updatekey list. That branch treats a missingweb_display_autostartas an unchecked box, so counting atarget_fps-only POST as a General-tab save would silently switch autostart off. There's a regression test for that too.Verification
End-to-end, with no plugin-side change:
Repeated on real hardware (devpi, 512×64, 22 plugins): set
target_fps: 90in the liveconfig.json, confirmed the property resolved it (90, andtimezonealongside it), and confirmed the deployedbaseball-scoreboardpath droveScrollHelper120 -> 90. The fourglobal_config-assigning plugins installed there (ledmatrix-stocks,news,ledmatrix-leaderboard,stock-news) all loaded clean, no tracebacks — the setter proven in situ. The rig was then restored byte-identical and restarted.15 new tests. Each was checked to fail without the implementation rather than passing vacuously — the
--apply-style trap of a test that can't fail. Suite: 1441 passed. Four failures (test_display_dirty_tracking,test_web_api::test_get_system_status, two intest_state_reconciliation) are pre-existing and reproduce identically on a clean tree, verified by stashing.What this does not do
It does not unify
scroll_display.py. The convergence plan calls for the core to ship one, and that's the reason thetarget_fpschange had to be written ten times. But the copies have genuinely diverged along the three documented lineages:Reconciling 500–670 line divergences is a project, not a PR, and doing it badly would be worse than not doing it. This PR delivers the piece that unblocks work already written; the unification is the follow-up.
🤖 Generated with Claude Code
https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
Summary by CodeRabbit
New Features
Bug Fixes