Skip to content

fix: eighteen validation issues across the plugin fleet - #470

Merged
ChuckBuilds merged 12 commits into
mainfrom
fix/plugin-fleet-issues
Sep 8, 2026
Merged

fix: eighteen validation issues across the plugin fleet#470
ChuckBuilds merged 12 commits into
mainfrom
fix/plugin-fleet-issues

Conversation

@ChuckBuilds

@ChuckBuildsChuckBuilds commented Sep 7, 2026

Copy link
Copy Markdown
Owner

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/LEDMatrix first, then guessed a sibling checkout, and
inserted whichever hit at sys.path[0] — beating the PYTHONPATH that
run_plugin_tests.py --core sets. The flag whose whole purpose is choosing a
core 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, %-d and %-I are glibc extensions;
strftime raises ValueError on Windows and musl. Fourteen call sites across
ten plugins. In baseball-scoreboard's scroll renderer the enclosing except
swallowed 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.py off glibc. Output is byte-identical on glibc.

fix(scoreboards)#427, #429, #426, #422, #432, #435, #373. Seven
settings-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:

  • soccer's 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 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_ranking away from its default and left the league
    blocks alone will see the league value win after this. The README says so.
  • afl and nrl's scroll_settings block was unreachable from either name
    (additionalProperties: false forbids writing scroll_mode, which is what
    the code read). Making it reachable also required moving scroll_speed's
    default 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_based rotation
implemented (day-of-year, the indexing of-the-day uses). skyaware_url no
longer defaults to 192.168.86.30 — one receiver on one home network, which
every 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 .bdf at the
size its PIXEL_SIZE header declares instead of silently substituting another
face (twelve of thirteen offered sizes failed before). ledmatrix-stocks gets
the test/harness.json it never had — all eight sizes were passing against a
blank 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 a config.json
still 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 caption
face 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, no
new files. Pomodoro's four images are regenerated and reproduce byte-for-byte
across runs. The 217 composites under docs/assets/ are deliberately left
alone: their captions change on the next regeneration of each, better done per
plugin than as one commit.

chore(store) — 20 manifests bumped, plugins.json regenerated.

#371, #375, #378, #391 are diagnosed but not closed here

The cause is the text layout engine, not Pillow. ImageFont.truetype picks it
at load time — Raqm where the host Pillow has libraqm, Basic otherwise — and
the two round fractional glyph advances differently. PressStart2P at 8px has
whole-pixel advances so both agree; 4x6-font.ttf at 6px does not. The four
drifting 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-golden run on this runner,
after #539 lands. The four plugins are byte-identical to main in this branch,
so nothing here blocks on that.

Checks

  • scripts/check_plugin.py32/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 main and reproduce identically
there, 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

🤖 Generated with Claude Code

ChuckBuildsand others added 8 commits September 7, 2026 16:01
…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>
@coderabbitai

coderabbitaiBot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 9a4d74b4-1081-4e8e-85be-e756977920da


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codacy-production

codacy-productionBot commented Sep 7, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics38 complexity

MetricResults
Complexity38

View in Codacy

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.

…-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

Copy link
Copy Markdown
OwnerAuthor

The safety-harness failure is the expected ordering, plus one real finding

CI's harness checks the branch out against core main, which does not yet
carry the layout-engine pin from ChuckBuilds/LEDMatrix#539.
So the four refreshed golden sets — christmas-countdown, geochron,
ledmatrix-weather, of-the-day, and no others — drift there. #539 has to merge
first
; nothing else in this PR depends on it.

That run was also the first real test of the diagnosis, because this runner has
libraqm and my machine does not. It confirmed the layout engine as the cause
and surfaced a second, smaller one:

  • christmas-countdown, of-the-day, weather draw through the core's
    draw_text, so their font loads are core-owned. Unpinned core → drift.
    Pinned core → they match. Nothing more to do here.
  • geochron loads its own face, so this branch already pinned it — and it
    still drifted in CI by 52–135px at max Δ=60–83. Low deltas mean edge
    coverage, not glyph position: the sidebar was drawn anti-aliased, and
    coverage varies with the host FreeType build even at the same Pillow
    version.

Fixed in f906a93: the sidebar draws 1-bit for the duration of the readout,
restoring the previous fontmode so nothing else on that shared canvas
changes. That is what an LED grid wants anyway — a half-lit pixel is a dim
pixel, not a smooth edge. The sidebar golden is now exactly three colours,
background plus the two text colours, which leaves nothing for a FreeType
build difference to disagree about. Goldens regenerated.

I would expect geochron to go green on this run even against the unpinned core,
and the other three to follow once #539 lands. If geochron still drifts here,
that is worth knowing and I will keep digging.

ChuckBuildsand others added 2 commits September 7, 2026 17:17
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

Copy link
Copy Markdown
OwnerAuthor

One more caught on a merge-safety pass — 91b7982

Re-reading #422 rather than trusting green CI, because "a dead config block
becomes live" means every key in it becomes an override, and one of them
disagreed with what the code had been resolving.

Both afl and nrl 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 — same trap as scroll_speed, which I only caught because its factor
was 50x rather than 2x. No test covers it and the harness renders frame 0,
where the gap does not show, so CI would not have told anyone.

The schema declares 24 now, and every key in the block agrees with the value
the resolver produced while it was unreachable:

keyschemaresolved before
scroll_speed50.050.0core default
scroll_delay0.010.01core default
gap_between_games2424plugin override — this commit
show_league_separatorstruetruecore default
dynamic_durationtruetruecore default
game_card_width128computedalready 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.

@ChuckBuilds
ChuckBuilds merged commit cbc539f into mainSep 8, 2026
4 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ChuckBuilds