镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Sports: recent-game date, 88 inert config controls hidden, rankings fetch gated (9 plugins) - #523

Merged
ChuckBuilds merged 7 commits into
mainfrom
fix/baseball-update-yields-within-budget
Sep 25, 2026
Merged

ChuckBuilds merged 7 commits into
mainfrom
fix/baseball-update-yields-within-budget

Conversation

@ChuckBuilds

Copy link
Copy Markdown
Owner

Found soaking on a 256×64 rig: every cold-cache start logged Plugin baseball-scoreboard update() timed out at 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 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 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 wait stays under the smallest slot measured on hardware, with headroom
    (the slot is smaller on a busier board);
  • it isn't re-inlined at the call site as a bare number, which is how it drifted
    from the core's budget in the first place;
  • a manager that has already started is not cancelled by the shutdown call —
    the property the whole fix rests on;
  • the warning names the slow managers and says the work continues.

Verification

  • All baseball tests pass, check_plugin.py green on all sizes, all 30 repo
    guard scripts pass, drift and collisions clean.
  • On the rig: no ERROR on a warm restart, and the card path confirmed working
    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

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
@coderabbitai

coderabbitai Bot commented Sep 23, 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: a405819e-1f60-4cb0-90d5-2feb42c01d98


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-production Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 478 complexity

Metric Results
Complexity 478

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.

ChuckBuilds and others added 2 commits September 24, 2026 19:52
…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
@ChuckBuilds

Copy link
Copy Markdown
Owner Author

Rebased onto main and resolved — this was CONFLICTING, now MERGEABLE.

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 update()-slot fix. Meanwhile main shipped #524 (patched every scoreboard), #528 (baseball 1.48.0) and #520 (hockey 1.33.0/1.33.1).

Twelve conflicts, all two structural kinds — generated plugins.json, and independent head-insertions into the versions arrays. sports.py merged cleanly in all nine.

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):

plugin main shipped ours was ours now
baseball 1.48.0 (Now Batting default), 1.47.1 (espn_dates) 1.48.0 1.49.0
baseball ↑ 1.47.1 (this PR's original fix) 1.48.1
hockey 1.33.1, 1.33.0 (goal-scorer card) 1.33.0 1.34.0

The other seven took patch bumps on main; 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 rather than merged textually.

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 — reproduces identically (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.

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.

@ChuckBuilds ChuckBuilds changed the title Baseball: return update() inside the slot the core grants it (1.47.1) Sports: recent-game date, 88 inert config controls hidden, rankings fetch gated (9 plugins) Sep 25, 2026
ChuckBuilds and others added 3 commits September 25, 2026 08:24
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>
@ChuckBuilds
ChuckBuilds merged commit c9eb3a4 into main Sep 25, 2026
5 checks passed
ChuckBuilds added a commit that referenced this pull request Sep 28, 2026
…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>
Sign up for free to 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.

2 participants