fix: eighteen validation issues across the plugin fleet - #470
Conversation
…tory Twenty test files across sixteen plugins probed `/home/rackpi/projects/LEDMatrix` first, then guessed a sibling checkout, and inserted whichever hit at `sys.path[0]`. Both beat the PYTHONPATH that `scripts/run_plugin_tests.py --core` sets, so the flag whose whole purpose is choosing a core was ignored whenever either candidate happened to exist — tests ran against the wrong checkout and reported pass or fail with equal confidence. They now read `LEDMATRIX_CORE`, which the runner sets to an absolute path for exactly this reason, and only guess at the sibling layout when nobody told them. The hardcoded home directory is gone from the repo. Closes#467 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ftime `%-m`, `%-d` and `%-I` are glibc extensions. On Windows and on musl (Alpine) `strftime` raises `ValueError`, so the fourteen call sites across ten plugins could not render a game card anywhere but a glibc host. In baseball-scoreboard's scroll renderer the damage was visible rather than fatal: the enclosing `except` swallowed the error, dropped the start time and drew the raw ISO date in its place, which is what made three of its upcoming goldens the only cards failing `test_scroll_card_renders.py` off glibc. Output is byte-identical on glibc, so no goldens move. `sports.py` is the copied-not-shared lineage, so all nine siblings are fixed here per docs/plugin-development/08-shared-sports-code.md. Closes#414Closes#447 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…th them Six settings-level defects across the scoreboard lineage, all the same shape: the schema and the web UI promise something the runtime never reads, or reads with a different default. * nrl: four `manager.py` fallbacks disagreed with `config_schema.json` (`recent_games_to_show` 5→1, `upcoming_games_to_show` 10→1, `show_odds` false→true, `update_interval_seconds` 300→3600), plus the same `show_odds` drift in `get_info()`. They only fire for a hand-written or partial config — and a fresh install before the UI has saved once. (#427) * lacrosse: `defaults.display_duration` fell back to 30 where the schema says 15, and `game_display_duration` was read but declared nowhere, so the UI could not offer it. Declared now, beside `display_duration`. (#429) * every scoreboard: `other_games_min_quality`'s help text described a `broadcast` value the enum does not offer — it was retired, and the code migrates it to `ranked`. 29 descriptions and three README references corrected. (#426) * afl and nrl: the `scroll_settings` block was unreachable. The scroll classes read `scroll_mode`, and `additionalProperties: false` meant a user could not write that name either, so all twelve settings were dead. `SCROLL_CONFIG_KEY` now names the block the schema declares; `scroll_speed`'s default moves 1.0 → 50.0 to match the core default the block had been shadowing, so nothing changes for an existing install. (#422) * hockey and lacrosse: the scroll and Vegas cards gated the shots line on a flat `show_shots` key at the league root that the schema declares nowhere, so the line never drew there whatever the user set. Both now walk the same ladder `manager.py` resolves for the switch card. (#432) * soccer: `leagues.<slug>.display_options` was read by nothing — 30 settings in the UI doing nothing, and the opposite precedence to every sibling. The league block now wins with the plugin-level key as fallback. Note the web UI writes a value into every league block, so after a save the per-league copy is the one in play; the README says so. (#435) * afl, nrl, soccer, ufc: `dynamic_duration.min_duration_seconds` was declared and never applied — only the cap was. A floor is resolved on the same ladder and applied after the cap, so an explicit minimum wins, matching what ledmatrix-leaderboard and cricket-scoreboard already do. `max_workers` is now passed to `get_background_service` instead of a hardcoded 1; its schema default moves 3 → 1 so behaviour is unchanged, and its description says the service is a process-wide singleton whose first caller decides. (#373) Closes#426Closes#427Closes#429Closes#422Closes#432Closes#435Closes#373 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…anger's LAN IP * static-image: `rotation_mode: "date_based"` was a stub returning the first image, so it looked like broken rotation rather than an unimplemented feature — no log line, the display simply never advanced. It now picks by day-of-year, the same indexing `of-the-day` uses: a set of seven repeats weekly, a set of 365 walks the year, and a given date always shows the same image. (#372) * ledmatrix-flights: `skyaware_url` defaulted to `192.168.86.30` — one particular receiver on one home network — in the schema and four code paths. Every install that did not set it polled that address on its own LAN and reported "no data received" rather than "not configured", and `check_plugin.py` spent ~40s per run waiting out connect timeouts. The default is empty now, and the fetcher says which setting is missing instead of guessing. (#400) Closes#372Closes#400 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…harness data * Six plugins still asked FreeType for a .bdf face at an arbitrary point size. BDF is a fixed-size bitmap format, so that raises "invalid pixel size" for every size but the file's own strike, and the loader then fell back to a different face with only a log warning — twelve of the thirteen offered sizes failed that way, so picking a bitmap font in the web UI appeared to do nothing. afl, basketball, nrl, soccer and ufc (shared sports.py lineage) and ledmatrix-music now retry at the size the file's PIXEL_SIZE header declares, the same fix already carried by baseball, football, clock-simple, news, tide-display, jellyfin-now-playing, mqtt-notifications and youtube-stats. (#387) `cozette.bdf` is already gone from every schema, and f1-scoreboard already pins bitmap faces to their native size through its own table. * ledmatrix-stocks shipped no `test/harness.json`, so the safety harness had no quote data, `stock_data` stayed empty, and all eight sizes passed against a blank frame — nothing about the ticker layout, fonts, colours or inline chart was being checked. It now has one, built on the recorded fixture from #394: a gain, a loss and a crypto entry. The base run uses switch mode, because frame 0 of a scroll is legitimately blank; a `scroll` variant keeps the scroll path under the harness too. All eight sizes now render real content with no empty warning. (#395) Closes#387Closes#395 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every setting below rendered in the web UI with a description promising specific behaviour, saved to config.json, and did nothing — verified against both the plugin source and the LEDMatrix core. BREAKING CHANGE: these keys are gone from the schemas, and all six plugins set `additionalProperties: false`. A config.json still carrying them fails schema validation until it is saved again from the web UI; the plugin still loads, but the UI flags its config as not matching the schema. * `transition.*` in christmas-countdown, nfl-draft, olympics and web-ui-info, plus `high_performance_transitions` in the first and third. Nothing in any of the four reads them, and the core implements no display transitions — every occurrence of "transition" under `src/` is a comment about plugin state machines or Vegas cycle boundaries. There is no `fade`, `slide`, `wipe`, `dissolve` or `pixelate` anywhere. If transitions are wanted later they belong in the core display controller, where every plugin gets them, not in five copied schema blocks. (#381, #377) * masters-tournament's whole `notifications` and `branding` blocks (12 settings), plus five leaves inside `display_modes` that the generic mode-handling never reaches: `player_cards.show_headshots`, `player_cards.duration_per_player`, `course_tour.show_animations`, `course_tour.duration_per_hole` and `course_tour.featured_holes`. The last one is not the `masters_featured_holes` display mode, which is live. (#418) * march-madness's `display_options.show_bracket_progress`, and the attribute it was assigned to — read once in `__init__`, never referenced again. (#406) `christmas-countdown`'s `tree_size` stays: it is read and validated, just not applied, which is a different defect and still documented in its README. Closes#381Closes#377Closes#418Closes#406 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…op host fonts **Why the goldens travelled badly.** `ImageFont.truetype` picks its layout engine at load time: Raqm where the host Pillow was built with libraqm, Basic otherwise, and the two round fractional glyph advances differently. `PressStart2P-Regular.ttf` at 8px has whole-pixel advances so both engines agree — which is why fifteen plugins' goldens matched everywhere and hid this. `4x6-font.ttf` at 6px does not: glyph positions drift cumulatively along a run. The four plugins that draw body text in it are exactly the four with open drift reports, and of-the-day's own issue records the tell — its PressStart2P title is pixel-identical while its 4x6 body is not. geochron, of-the-day, ledmatrix-weather and christmas-countdown now pin the Basic engine where they load their own faces, and the four golden sets are regenerated against a core doing the same (ChuckBuilds/LEDMatrix fix/deterministic-text-rendering). All 32 plugins now pass `check_plugin.py` here; the other fifteen golden sets are untouched and still pass, which is the check that this pinned the right thing rather than papering over it. **Host fonts in the documentation renderers.** `scripts/render_docs_assets.py` and `plugins/pomodoro-timer/test/render_readme_assets.py` both picked their caption face from a list of host font paths — DejaVu on Linux, Segoe UI on Windows, and for the pomodoro one no Windows path at all, so it fell through to PIL's tiny default and rewrote every labelled contact sheet on a plugin nobody had touched. Both now use `ImageFont.load_default(size=...)`, which Pillow >= 10.1 serves from bundled Aileron: no font files, no host lookup. Aileron ships one cut, so bold is a point larger, and it has no em dash or multiplication sign, so the pomodoro captions are ASCII — both rendered as a .notdef box. Its four images are regenerated and now reproduce byte-for-byte across runs. The committed composites under `docs/assets/` are left as they are: their captions will change on the next regeneration of each, which is a mechanical pass better done per plugin than as one 217-image commit. Closes#371Closes#375Closes#378Closes#391Closes#383 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Six majors for the schema removals — christmas-countdown, nfl-draft, olympics, web-ui-info, masters-tournament and march-madness — because a config.json still carrying a removed key fails validation until it is saved again. Eight minors where a declared setting starts working or a default changes, nine patches for portability and rendering fixes that leave every existing panel drawing what it drew before. `plugins.json` regenerated with `update_registry.py`; it is never hand-edited. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 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 |
Up to standards ✅🟢 Issues |
| Metric | Results |
|---|---|
| Complexity | 38 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewerTIP This summary will be updated as you push new changes.
…-specific Pinning the layout engine fixed glyph *positions*, and CI showed what it did not fix: the sidebar was still drawn anti-aliased, and edge coverage varies with the host FreeType build. geochron draws straight onto the display manager's canvas with its own font, so it never inherited the core's 1-bit text setting the way plugins going through `draw_text` do. The text is now drawn 1-bit for the duration of the readout, restoring the previous `fontmode` afterwards so nothing else on that shared canvas changes. That is what an LED grid wants regardless — a half-lit pixel is a dim pixel, not a smooth edge — and it removes the last variable from the render: the sidebar is exactly three colours now, background plus the two text colours. Goldens regenerated. Refs #375 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ChuckBuilds
commented
Sep 7, 2026
The safety-harness failure is the expected ordering, plus one real findingCI's harness checks the branch out against core That run was also the first real test of the diagnosis, because this runner has
Fixed in I would expect geochron to go green on this run even against the unpinned core, |
…ing host-specific" This reverts commit f906a93.
CI is the only machine that can settle this, and it says the refresh is not finished. This runner has libraqm and mine does not, which is why the drift reports existed at all — and after pinning the layout engine plugin-side, geochron still drifted here by 135px at max Δ=60–83, unchanged by the 1-bit experiment in the commit reverted above (geochron's text was already 1-bit; that change was a no-op and is gone). So the four golden sets and the plugin-side engine pins come out, restoring geochron, of-the-day, christmas-countdown and ledmatrix-weather to exactly what `main` has, and their three version bumps come out with them. What stays is everything that does not depend on a Linux render: eighteen issues, and the two documentation renderers from #383, whose determinism is about host *font files* rather than golden images and which pomodoro's harness already passes. The diagnosis stands and is recorded on the four issues. The core half is ChuckBuilds/LEDMatrix#539, which is independently correct and green. The golden refresh wants doing from a Linux run — a `--update-golden` on this runner — rather than guessed at from Windows, which is the mistake this reverts. Refs #371, #375, #378, #391 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…achable Caught reviewing #422 for merge safety rather than by a test, because no test covers it and the harness renders frame 0, where the gap between cards does not show. Making the block reachable means every key in it becomes an override, and one of them disagreed with the value the code had been resolving. Both plugins set `gap_between_games: 24` in `scroll_settings_defaults()`, overriding the core's 48; the schema block declared 48. So the fix for #422 would have silently doubled the gap between game cards on every afl and nrl install — the same trap as `scroll_speed`, which I caught only because its factor was 50x rather than 2x. The schema now declares 24, and every key in the block agrees with what the resolver produced while it was unreachable: scroll_speed 50.0 (core default) scroll_delay 0.01 (core default) gap_between_games 24 (plugin override) <- this commit show_league_separators true (core default) dynamic_duration true (core default) game_card_width 128 (already guarded as "unchosen") `game_card_width` needed nothing: both plugins already treat a value equal to the schema default as unchosen and substitute their computed width, which is the same guard this needed and did not have. Reachability is now behaviour-neutral for a default config, which is what #422 should have been all along. READMEs corrected. Refs #422 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ChuckBuilds
commented
Sep 8, 2026
One more caught on a merge-safety pass — |
| key | schema | resolved before | |
|---|---|---|---|
scroll_speed | 50.0 | 50.0 | core default |
scroll_delay | 0.01 | 0.01 | core default |
gap_between_games | 24 | 24 | plugin override — this commit |
show_league_separators | true | true | core default |
dynamic_duration | true | true | core default |
game_card_width | 128 | computed | already guarded as "unchosen" |
game_card_width needed nothing: both plugins already treat a value equal to
the schema default as unchosen and substitute their computed width — the same
guard this needed and did not have.
Reachability is behaviour-neutral for a default config now, which is what #422
should have been all along. READMEs corrected, harness and unit tests green for
both plugins.
Uh oh!
There was an error while loading. Please reload this page.
Closes 18 of the open validation issues in one branch, so CodeRabbit sees one
diff instead of twenty. Eight commits, each a self-contained theme — reviewing
commit-by-commit is the sane order.
Companion core PR: ChuckBuilds/LEDMatrix#539 (needed for the golden refresh
below; nothing else here depends on it).
What is in it
fix(tests)— #467. Twenty test files across sixteen plugins probed/home/rackpi/projects/LEDMatrixfirst, then guessed a sibling checkout, andinserted whichever hit at
sys.path[0]— beating the PYTHONPATH thatrun_plugin_tests.py --coresets. The flag whose whole purpose is choosing acore was ignored whenever either candidate existed, so tests ran against the
wrong checkout and reported pass or fail with equal confidence. They now read
LEDMATRIX_CORE, and the home directory is gone from the repo.fix(sports)— #414, #447.%-m,%-dand%-Iare glibc extensions;strftimeraisesValueErroron Windows and musl. Fourteen call sites acrossten plugins. In baseball-scoreboard's scroll renderer the enclosing
exceptswallowed it, dropped the start time and drew a raw ISO date instead — which is
what made three upcoming goldens the only cards failing
test_scroll_card_renders.pyoff glibc. Output is byte-identical on glibc.fix(scoreboards)— #427, #429, #426, #422, #432, #435, #373. Sevensettings-level defects of one shape: the schema and the web UI promise
something the runtime never reads, or reads with a different default. The two
worth calling out:
leagues.<slug>.display_optionswas read by nothing — 30 settings inthe UI doing nothing, and the opposite precedence to every sibling. The
league block wins now. Caveat the issue did not have: the web UI writes a
schema default into every league block, so an install that set the root
show_records/show_rankingaway from its default and left the leagueblocks alone will see the league value win after this. The README says so.
scroll_settingsblock was unreachable from either name(
additionalProperties: falseforbids writingscroll_mode, which is whatthe code read). Making it reachable also required moving
scroll_speed'sdefault 1.0 → 50.0, because the block had been shadowing the core default —
without that, every afl/nrl panel would have slowed to a crawl on upgrade.
On #373's
background_service.max_workers: removing it would have been wrong.The service is a process-wide singleton, so a per-plugin worker count cannot
mean what it says — the first plugin to start decides for everyone. It is wired
through instead, its default corrected 3 → 1 to match the behaviour every
install has actually had, and its description now says so.
fix(static-image, flights)— #372, #400.date_basedrotationimplemented (day-of-year, the indexing
of-the-dayuses).skyaware_urlnolonger defaults to
192.168.86.30— one receiver on one home network, whichevery unconfigured install polled on its own LAN, and which cost the harness
~40s of connect timeouts per run.
fix(fonts, stocks)— #387, #395. Six more plugins retry a.bdfat thesize its
PIXEL_SIZEheader declares instead of silently substituting anotherface (twelve of thirteen offered sizes failed before).
ledmatrix-stocksgetsthe
test/harness.jsonit never had — all eight sizes were passing against ablank frame, so nothing about its ticker layout, fonts, colours or inline chart
was being checked.
feat!: remove seventeen settings— #381, #377, #418, #406. Per your call.Breaking: all six plugins set
additionalProperties: false, so aconfig.jsonstill carrying a removed key fails validation until it is saved again — the
plugin still loads, the UI flags the config. Majors on all six.
fix(render)— #383 only. Both documentation renderers picked a captionface from a list of host font paths, and the pomodoro one listed no Windows
path at all, so a Windows rerun fell through to PIL's default face and rewrote
three of four contact sheets on a plugin nobody had changed. Both use
ImageFont.load_default(size=...)now — bundled Aileron, no host lookup, nonew files. Pomodoro's four images are regenerated and reproduce byte-for-byte
across runs. The 217 composites under
docs/assets/are deliberately leftalone: their captions change on the next regeneration of each, better done per
plugin than as one commit.
chore(store)— 20 manifests bumped,plugins.jsonregenerated.#371, #375, #378, #391 are diagnosed but not closed here
The cause is the text layout engine, not Pillow.
ImageFont.truetypepicks itat load time — Raqm where the host Pillow has libraqm, Basic otherwise — and
the two round fractional glyph advances differently.
PressStart2Pat 8px haswhole-pixel advances so both agree;
4x6-font.ttfat 6px does not. The fourdrifting plugins are exactly the four drawing body text in the 4x6 face, and
#371 recorded the tell without naming it: of-the-day's PressStart2P title is
pixel-identical while its 4x6 body is not.
The core half is ChuckBuilds/LEDMatrix#539,
which pins the engine and is green.
I had the golden refresh in this branch and took it back out, because this
runner has libraqm and my machine does not — so CI is the only place that can
settle it, and it said the refresh was not finished. Even with the engine
pinned plugin-side, geochron still drifted here by 135px. Regenerating goldens
from Windows is guessing; they want a
--update-goldenrun on this runner,after #539 lands. The four plugins are byte-identical to
mainin this branch,so nothing here blocks on that.
Checks
scripts/check_plugin.py— 32/32 plugins, 0 failures, against #539.scripts/check_module_collisions.py— clean across 43 plugins.scripts/check_manifest_version_fields.py— clean across the 23 bumped.scripts/run_plugin_tests.py --all— 245 passed, 1 skipped, 2 failed.The two failures are pre-existing on
mainand reproduce identicallythere, unchanged by this branch:
baseball-scoreboard/test_odds_placement.py— 2 checks, both odds y-placement(
stays at the top when the O/U is short: y=8).ledmatrix-weather/test_almanac_layout.py— 1 check(
time-mode 128x64: expected full am/pm on 128-wide, got compact).CI on this branch is green — safety, collisions and Codacy all pass.
I have not touched them; say the word if you want them in scope.
Not closed
main; I verifiedeach against the current tree rather than assuming. Worth closing.
fix(starlark): restore the API routes #330 dropped), which is on coremainnow.that issue flagged:
calendarmeasures withtextbboxitself,clock-simpleand
ledmatrix-flightsnever hand a rawfreetype.Facetoget_font_height().ledmatrix-musicwas the only one exposed and wasalready guarded.
🤖 Generated with Claude Code