Repository navigation
Sports: recent-game date, 88 inert config controls hidden, rankings fetch gated (9 plugins) - #523
Conversation
Found soaking on a 256x64 rig: every cold-cache start logged "Plugin baseball-scoreboard update() timed out" at ERROR. At startup the display controller hands each plugin whatever is left of a shared deadline (display_controller._update_plugins), so the slot shrinks as it works down the plugin list. Baseball's slot was 15.4s and 15.96s on two consecutive boots -- the varying value is what gave it away as a share of a deadline rather than a fixed timeout. This plugin waited 25s on its six parallel managers. A 25s wait inside a ~15s slot can only ever end one way: the core kills the call and logs an ERROR, and this method's own timeout branch -- the one that names *which* managers are slow -- never runs. The plugin's timeout was longer than the budget it was given, so it could never use it. The wait is now 10s, in a named constant documented against the core's budget, with a test asserting it stays under the smallest slot measured on hardware and is not re-inlined at the call site. Nothing was lost before and nothing is lost now, which is what makes yielding early safe: shutdown(cancel_futures=True) only cancels managers that have not started, and all six start immediately, so in-flight fetches run to completion and populate their caches either way. A plugin cut off at startup is also immediately due again, so the scheduled tick picks it up -- the core's own docstring says as much. The difference is that returning under our own steam turns a core-level ERROR into a plugin WARNING naming the slow managers. The expensive part of a cold start is the NCAA baseball season fetch -- 5,500 events, ~10s on the rig -- which the background service already runs off-thread; this does not change that. Verified on the rig: baseball 1.47.0 plus this change, no ERROR on a warm restart, and the card path confirmed working end-to-end on a live MLB game (headshot prefetch wrote four new 192x192 files, zero draw errors). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9
|
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 | 478 |
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.
…t cannot affect anything (#527) * feat(baseball): date on recent games; hide config that cannot do anything (1.48.0) Adds the date of a finished game to both Recent displays, and stops drawing five settings that are inert in every baseball league. THE DATE. The full-screen (switch) Recent scoreboard has drawn one along its bottom edge since it was written, but it drew game_date raw -- the "9/23" the extractor emits -- so it was the one date on the board that ignored every date-format setting: a panel set to "abbrev" read "Sep 23" on its upcoming screen and "9/23" here. It now goes through switch_date_format like the upcoming screen, which changes nothing on an untouched panel because that key defaults to "numeric" and numeric returns the raw text. New scroll_card.switch_recent_show_date (default true) is the off switch that did not exist. The scroll and Vegas Recent card drew no date at all. Five of the eight sibling scoreboards already draw one there, so this closes a drift gap -- but their placement does not transfer: they centre the recent score and leave the bottom edge free, while this card puts the score at display_height-14, so the free strip is above it. scroll_card.recent_show_date (default false) adds the date, formatted by the shared date_format; scroll_card.recent_date_position picks own_row, top_line, or auto, which measures the fonts and panel in use rather than assuming a size. An explicit own_row with no room draws nothing rather than overprinting the score. Verified at all eight harness sizes in all three positions. check_plugin's own mlb_recent render is empty (27 lit pixels -- no finished game in its mock slate), so that PASS proves nothing about this card; it was rendered directly instead, bounds-checked with records and odds on. Both paths also confirmed on a 512x64 panel. INERT SETTINGS, now "x-display": "hidden" -- still declared, so a config already carrying one keeps validating and nothing is lost on upgrade: * other_games_divisions (all three leagues) -- ESPN publishes FBS/FCS group rosters for college-football alone, so the lookup resolves nothing and the filter fails open. * other_games_min_quality and show_ranking (all three) -- no baseball league publishes a poll. NCAA Baseball included: measured 2026-09-24, baseball/college-baseball/rankings answers 404 while /scoreboard and /standings on the same slug answer 200. Out of season is not the explanation -- men's college lacrosse and hockey are equally out of season and both answer 200 with real poll blocks. show_ranking was worse than inert, because the rank badge replaces the record, so ticking it erased the records show_records was drawing. * scroll_delay -- its own description has read "ignored" for releases. * customization.layout.ranking -- no reader; layout.record positions the badge. THE FETCH. _league_has_rankings gated the quality-filter call site but not the two show_ranking ones. The gate moved to the top of _fetch_team_rankings, covering every caller at once, and is narrowed by one measured exception (_NO_POLL_COLLEGE_LEAGUES) so NCAA Baseball's old "ranked" default stops requesting too. Its default is now "any" -- what every board already showed, since "ranked" with no poll lets everything through. MILB LEVELS. milb.sport_ids (Triple-A / Double-A / High-A / Single-A) is now declared and forwarded. BaseMiLBManager has read it since it was written, but _adapt_config_for_manager is a whitelist that never carried the key, so no value a user could write ever arrived -- which is also why the schema had never declared it. Empty or unusable input falls back to all four levels. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ET8e5weDrb5Ju5QTKLU7zh * fix(sports): stop drawing 75 config controls that cannot affect anything The same audit that produced baseball 1.48.0, applied to the other eight lineages. The sports scoreboards are copies of each other, and a schema is easier to copy than to re-derive, so three settings were carried into plugins whose leagues cannot use them. HIDDEN -- "x-display": "hidden", so each stays declared and a config already carrying it keeps validating; only the form control goes away: soccer 32, basketball 13, hockey 8, afl 6, nrl 6, football 5, lacrosse 4, ufc 1. * other_games_divisions (25) -- the FBS/FCS/Other checkboxes. ESPN publishes those group rosters for college football alone (_DIVISION_GROUPS_BY_LEAGUE, identical in all nine copies), so everywhere else the lookup resolves nothing and the filter fails open. Soccer offered them on the Champions League and the World Cup. * other_games_min_quality (19) -- "ranked" needs a poll; without one the check fails open and the setting has one meaningful value. * show_ranking (16) -- worse than inert. The rank badge REPLACES the record, so on a pollless league ticking it erased the records show_records was drawing. * scroll_delay (14) -- every copy's own description has read "Kept so saved configs still load; ignored" for releases. ufc reads it into a local and discards it, with a comment saying so. * customization.layout.ranking (1, basketball) -- no reader. WHICH LEAGUES HAVE A POLL IS MEASURED, not inferred from the "college"/"ncaa" heuristic. ESPN's /rankings was probed for every scoreboard league on 2026-09-24: college-football 200 (125 teams) mlb, nba, wnba, nfl, nhl 404 mens/womens-college-basketball 200 (50 each) minor-league-baseball 400 mens/womens-college-hockey 200 (4 / 5) afl, rugby-league 3 404 mens/womens-college-lacrosse 200 (28 / 33) soccer eng.1 404 college-baseball 404 <-- the heuristic's blind spot Left visible accordingly: football ncaa_fb, basketball ncaam/ncaaw, hockey and lacrosse ncaa_mens/ncaa_womens. Asserted, not assumed -- see scripts/test_inert_settings_are_hidden.py. THE FETCH. _league_has_rankings gated the quality-filter call site but not the show_ranking ones, so ticking Show Ranking on a pollless league sent two requests an hour to endpoints that cannot answer. The gate moved to the top of _fetch_team_rankings in all eight, covering every caller and unable to drift apart again. lacrosse and ufc serve no pollless league, so the port changes nothing there; it is carried because sports.py is copied, not shared, and a fix living in eight of nine copies is how the last one gets missed. NOT DONE, deliberately: on a pollless league an empty rank table makes show_ranking ERASE the record rather than fall back to it, in every lineage except basketball, whose _get_team_annotation already returns the record. Hiding and gating covers anyone configuring fresh, but a config saved with show_ranking: true still hits it. Porting basketball's fallback would also change what a poll-backed college board draws for an unranked team (blank today, the record after), so it is a display decision rather than cleanup. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ET8e5weDrb5Ju5QTKLU7zh * test(sports): enforce that no scoreboard draws an inert control scripts/test_inert_settings_are_hidden.py encodes the measured league->poll map and fails if any of the nine lineages draws other_games_divisions outside college football, or other_games_min_quality / show_ranking on a league with no poll, or scroll_delay, or customization.layout.ranking. It also checks the other half of the promise -- that the poll-backed blocks are still offered -- that every hidden property explains itself, and that _fetch_team_rankings consults _league_has_rankings in every copy. Encoding the map here rather than in nine schemas means the next league ESPN adds or drops is one edit, and a schema that quietly regains a dead control fails CI instead of shipping. Two fixes it flushed out: * test_poll_choice.py pinned its probe to probe.league = <plugin id>, which is not a league name any lineage ever sees. Harmless while nothing consulted it; now that _fetch_team_rankings is gated, a probe claiming to be in league "baseball-scoreboard" short-circuits and the badge table comes back empty. It now uses a synthetic "ncaa-poll-probe" -- deliberately not a real slug, since "college-baseball" would also fail against baseball's measured exception. * sports_drift_baseline.json records SportsCore._league_has_rankings as intended divergence. baseball narrows the shared heuristic by one league; ufc carries an inline copy of the original. The difference is deliberate and documented at both sites. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ET8e5weDrb5Ju5QTKLU7zh --------- Co-authored-by: Claude <noreply@anthropic.com>
…hile main moved a long way while this branch was open: #524 patched every scoreboard, #528 shipped baseball 1.48.0, #520 shipped hockey 1.33.0/1.33.1. Twelve conflicts, all of the two kinds this repo produces structurally -- generated plugins.json, and independent head-insertions into the versions arrays. sports.py merged cleanly in all nine. VERSION COLLISIONS. Two numbers were claimed by different changes on the two sides. Both resolved by keeping BOTH entries and re-releasing ours above main's -- never taking one side wholesale, which silently drops a shipped changelog entry: baseball main 1.48.0 (Now Batting default) and 1.47.1 (espn_dates fetch) ours 1.48.0 (recent date + inert config) -> 1.49.0 #523 1.47.1 (update() slot fix) -> 1.48.1 hockey main 1.33.1, 1.33.0 (goal-scorer card) ours 1.33.0 (inert config) -> 1.34.0 The other seven took patch bumps on main and ours sit above them unchanged. Hockey's README version stamps followed to 1.34.0. plugins.json was reset to main's copy and regenerated with update_registry.py rather than merged textually -- a textual merge of a generated file produces invalid JSON, and hand-editing leaves it inconsistent with the manifests. NOT FIXED, because it is main's and predates this branch: main's changelogs already lag their manifests -- baseball 1.47.1 and hockey 1.33.1 have manifest entries with no changelog section. Main's sections are preserved verbatim here rather than rewritten. Verified on the merged tree: all nine suites (afl 26, baseball 48, basketball 37, hockey 40, lacrosse 29, nrl 21, soccer 38, ufc 20 -- 0 failed), the inert config invariant (34 checks), drift, collisions, registry, and version bumps for all nine against main. Two pre-existing failures confirmed NOT caused here: * football test_espn_date_ranges::test_the_lookback_window_recovers_from_a_ rejected_range -- identical 62/1/1 with this branch's football edits reverted, and passes in isolation. * scripts/test_check_min_core_version.py -- its plugin-tree checks all pass; the failing one compares the script's table against the local LEDMatrix core checkout's tags, which no plugin change can affect. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ET8e5weDrb5Ju5QTKLU7zh
|
Rebased onto main and resolved — this was What changed since this PR was opened: #527 was merged into this branch, so it now carries the whole sports config audit as well as the original Twelve conflicts, all two structural kinds — generated Two version numbers were claimed by different changes on each side. Resolved by keeping both entries and re-releasing ours above main's, rather than taking either side wholesale (which would silently drop a shipped changelog entry):
The other seven took patch bumps on main; ours sit above them unchanged. Hockey's README version stamps followed to 1.34.0. Verified on the merged tree: all nine suites (afl 26, baseball 48, basketball 37, hockey 40, lacrosse 29, nrl 21, soccer 38, ufc 20 — 0 failed), the inert-config invariant (34 checks), drift, collisions, registry, and version bumps for all nine against Two pre-existing failures, confirmed not caused here:
Left alone deliberately: main's changelogs already lag their manifests (baseball 1.47.1 and hockey 1.33.1 have manifest entries with no changelog section). That predates this branch, so main's sections are preserved verbatim rather than rewritten here. The title has been updated — it still said "(1.47.1)", which this PR outgrew. |
main shipped hockey-scoreboard 1.33.2 (goal-card team colours) and 1.34.0 (game-activity pop-ups) in #530 while this branch carried its own 1.34.0 (hidden inert controls, gated rankings fetch). This branch's release is re-numbered 1.35.0 and stacked on top of main's two entries in the manifest and CHANGELOG, the README's "since 1.34.0" notes follow it, and plugins.json is resynced. Picks up main's MODULE_FIRST_VERSION entries for core #629/#630/#634, which is what test_check_min_core_version was failing on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Five manifests picked up raw UTF-8 (em dashes, the ▲/▼ indicator) in older release notes when this branch re-serialised them; main stores those characters as \u escapes. The core's load_manifest() opens the file in the platform encoding, which is cp1252 on Windows, so a raw UTF-8 byte aborts a harness run there. Escaped back; the parsed JSON is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#532 released a 1.48.1 of its own (card fonts, live score spacing). This branch's unreleased 1.48.1 (update() inside its slot) is folded into its 1.49.0 entry, and main's 1.48.1 sits below it. plugins.json regenerated from the manifests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…535) Every control hidden here was traced to no reader. The keys stay declared, so saved configs keep validating; nothing a board draws or fetches changes. - update_interval_seconds (root, per league, soccer custom leagues) and hockey/lacrosse update_intervals.base: SportsCore stores it and every manager overwrites it with its live/recent/upcoming interval before any read; the core scheduler reads update_interval, not this key. - root update_interval in football/basketball/baseball: the manifest's update_interval (60) wins in the core scheduler. - hockey/lacrosse defaults.season_cache_duration_seconds and display_durations.base (hockey/lacrosse/basketball): no reader. - root show_ranking in soccer/afl/nrl: no poll, and the badge replaces the record. The league copies were hidden in #523; these still reached the switch view. The soccer/afl/nrl READMEs already claimed them hidden. Also corrects the get_update_interval docstring in seven copies (it said the scheduler falls back to update_interval_seconds, 3600 by default) and a soccer comment that promised the key controlled fetch cadence. test_inert_settings_are_hidden.py gains the rules for these keys, scoped to first-party scoreboards; it fails on the previous schemas (44 exposed). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Found soaking on a 256×64 rig: every cold-cache start logged
Plugin baseball-scoreboard update() timed outat ERROR.The mechanism
At startup the display controller hands each plugin whatever is left of a
shared deadline (
display_controller._update_plugins), so the slot shrinksas it works down the plugin list. Baseball's slot was 15.4s and 15.96s on
two consecutive boots — the varying value is what gave it away as a share of
a deadline rather than a fixed timeout.
This plugin waited 25s on its six parallel managers. A 25s wait inside a
~15s slot can only ever end one way: the core kills the call and logs an ERROR,
and this method's own timeout branch — the one that names which managers are
slow — never runs. The plugin's timeout was longer than the budget it was
given, so it could never use it.
The fix
The wait is now 10s, in a named constant documented against the core's budget.
Yielding early is safe, and that's the part worth checking rather than assuming:
shutdown(cancel_futures=True)only cancels managers that have not started,and all six start immediately — so in-flight fetches run to completion and
populate their caches either way. A plugin cut off at startup is also
immediately due again; the core's own docstring says exactly that. So nothing
was lost before and nothing is lost now. The difference is that returning under
our own steam turns a core-level ERROR into a plugin WARNING naming the slow
managers.
The expensive part of a cold start is the NCAA baseball season fetch — 5,500
events, ~10s — which the background service already runs off-thread. This
doesn't change that, and isn't trying to.
Tests
Four, in
test_update_yields_within_budget.py:(the slot is smaller on a busier board);
from the core's budget in the first place;
the property the whole fix rests on;
Verification
check_plugin.pygreen on all sizes, all 30 repoguard scripts pass, drift and collisions clean.
end-to-end on a live MLB game — the headshot prefetch wrote four new
192×192 files and there were zero draw errors.
🤖 Generated with Claude Code
https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9