Skip to content

fix(sports): make the schedule-window settings actually take effect - #293

Merged
ChuckBuilds merged 4 commits into
mainfrom
fix/schedule-window-plumbing
Aug 19, 2026
Merged

fix(sports): make the schedule-window settings actually take effect#293
ChuckBuilds merged 4 commits into
mainfrom
fix/schedule-window-plumbing

Conversation

@ChuckBuilds

Copy link
Copy Markdown
Owner

Builds on #286 (rebased onto main, so this includes that work and supersedes it — #286 is currently conflicted anyway).

The settings never reached the code

Every plugin hands its managers a config built by _adapt_config_for_manager, and each of those adapters constructs its output key by key. Neither window key was named in any of the nine, so both were dropped in transit. SportsCore then did config.get(...)None_clamp_window fell back. Every user was pinned to the defaults, silently, with nothing logged.

Verified by running all nine adapters for real, with both keys set:

before:  afl/baseball/basketball/football/hockey/lacrosse/nrl/soccer/ufc
         lookahead=LOST   lookback=LOST     (all nine)

after:   lookahead=root   lookback=root     (all nine)

Three more reasons it didn't work end to end

The authoritative fetch ignored the setting. #286 parameterised _get_weeks_data(), which is the stand-in serving the display while the background fetch is in flight. For soccer, afl and nrl the real fetch lives in *_managers.py with its own hard-coded fortnight — so a widened window appeared briefly and then vanished when the real data replaced it. Those three now read the same configured values (they already inherit SportsCore).

Four plugins couldn't show the setting at all. afl, basketball, nrl and soccer declare a top-level x-propertyOrder, and the Jinja partial that serves the settings page iterates only that list — a property missing from it never renders. Added to all four.

schedule_lookback_days above 21 was inert. The Recent screen filtered on a fixed 21-day cutoff while the schema allows 60 and tells the user to "raise it if finished games disappear sooner than you want". Now follows the setting, in all ten files that carried it. Worth noting the fixed 21 was already unreachable in practice — the fetch only went back a fortnight — so this is less a behaviour change than the two numbers finally agreeing.

A bug my own change introduced, and how it surfaced

The cutoff reads through getattr(self, "schedule_lookback_days", _DEFAULT_LOOKBACK_DAYS) because managers are also constructed without __init__ — the plugin test suites do exactly that. A bare attribute access raised AttributeError into the surrounding except and silently skipped the spoiler filter for excluded teams. Two existing tests (football and lacrosse test_favorite_live_boost.py) caught it, which is the only reason it isn't in this PR as a regression.

_clamp_window also catches OverflowError now: json parses bare Infinity by default and int(inf) raises, which the existing TypeError/ValueError guard missed.

Testing

scripts/test_schedule_window_plumbing.py runs each of the nine adapters and asserts both keys survive. Grepping for the key names would not have caught this — one plugin forwards its config with **self.config in a different method, which reads like a passthrough.

Mutation-checked: removing the plumbing from the nine adapters fails all 18 checks; restoring it passes all 18.

soccer/test_schedule_horizon.py derived the full fetch's horizon from timedelta literals to prove the partial wasn't narrower. Those literals are gone, so it now asserts the stronger property: both fetches read the same configured values and neither carries a hard-coded window.

All 95 test files across the nine plugins pass.

Nine manifests bumped, plugins.json regenerated.

Note on #287

#287 stacks on #286 and its two settings are dropped by the same adapters. The plumbing here is per-key; #287 will need its own two keys added to the same loop, or the loop generalised to forward a list of root-level keys.

🤖 Generated with Claude Code

https://claude.ai/code/session_01STMbQE4YctTacQXfbYqKuW

claude added 3 commits August 14, 2026 21:17
Reported by a user: Manchester United never appeared even though their
next fixture was 22 August, and with favourites turned off the board
showed exactly one Premier League game, Arsenal v Coventry.

_get_weeks_data() is the partial that serves the display until the
background fetch lands. It looked ahead seven days; _fetch_soccer_api_data(),
the fetch it substitutes for, looks ahead fourteen. So a fixture inside
the real window was simply missing from the board.

That gap is invisible in a league that plays daily and severe in one that
plays weekly, where a whole matchweek can fall inside it. Reproduced
against ESPN on 2026-08-14:

  -2w..+1w  20260731-20260821 ->  1 event   (COV @ ARS, the 21st)
  -2w..+4w  20260731-20260911 -> 30 events  (MAN @ HUL on the 22nd, ...)

The Premier League's opening matchweek was 21-24 August, so a +7d horizon
caught the Friday opener and hid the other nine fixtures -- exactly the
single game the user described.

Both horizons now come from one pair of module constants, so the partial
cannot silently end up narrower than the fetch it stands in for again.
The test reads the full fetch's span out of soccer_managers.py and
requires the partial to be at least as wide, rather than hard-coding
either number.

Mutation-checked: reverting the horizon, re-hardcoding the window in
_get_weeks_data, and narrowing the back-window are all caught.

Harness clean; soccer's other tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5
How far back and forward games are fetched was hard-coded at 14 days back
and 7 forward, in all nine scoreboards.

Days is the wrong unit for a league that plays weekly: a whole matchweek
can fall just past a short horizon and never reach the board, which is
how a user's favourite team went missing while other clubs in the same
league appeared. Both sides are now advanced settings --
schedule_lookback_days and schedule_lookahead_days -- bounded 1 to 60,
with a non-numeric or out-of-range value falling back to the default
rather than raising.

Defaults keep today's behaviour, so nobody's board changes on upgrade.
The exception is soccer, whose lookahead moves 7 -> 14 to match the fetch
its partial stands in for; that is the reported bug.

This is an escape hatch, not the final answer. The right primitive is a
game count rather than a day count -- "the next five fixtures" spans five
days for a daily league and five weeks for a weekly one, so any fixed
number of days is wrong for one of them. That change wants gating on
whether favourite teams are configured, so a user browsing a whole league
does not pull a season of fixtures to show five.

Harness clean on a four-plugin spot check; the two failures across the
nine plugins' test suites are the pre-existing missing-src ones also
present on main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5
@coderabbitai

coderabbitai Bot commented Aug 18, 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: Pro Plus

Run ID: 1a80b439-a49b-4e97-9139-9d41fea3d696


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 Aug 18, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 46 complexity

Metric Results
Complexity 46

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
ChuckBuilds force-pushed the fix/schedule-window-plumbing branch 2 times, most recently from 570fcaa to 2da6c51 Compare August 18, 2026 22:17
Builds on #286, which added schedule_lookback_days / schedule_lookahead_days
across nine scoreboard plugins. The settings never reached the code.

Every plugin hands its managers a config built by
_adapt_config_for_manager, and each of those adapters constructs its output
key by key. Neither window key was named in any of them, so both were
dropped on the way through. SportsCore then did config.get(), got None,
and _clamp_window fell back -- every user pinned to the defaults, nothing
logged. Verified by running all nine adapters for real: with both keys set
in the plugin config, the adapted config contained neither, at the root or
nested. They are now carried across, and the same probe shows both landing
at the root for all nine.

Three further reasons the feature did not work end to end:

- The authoritative fetch ignored the setting. #286 parameterised
  _get_weeks_data(), which is the stand-in that serves the display while
  the background fetch is in flight. For soccer, afl and nrl the real fetch
  lives in *_managers.py and carried its own hard-coded fortnight, so a
  widened window showed briefly and then vanished when the real data
  replaced it. Those three now read the same configured values -- they
  already inherit SportsCore, so the attributes were there.

- Four of the nine could not show the setting at all. afl, basketball, nrl
  and soccer declare a top-level x-propertyOrder, and the Jinja partial
  that serves the settings page iterates only that list, so a property
  missing from it never renders. Both keys added to all four.

- schedule_lookback_days above 21 was inert. The Recent screen filtered on
  a fixed 21-day cutoff while the schema allows 60 and tells the user to
  "raise it if finished games disappear sooner than you want". The cutoff
  now follows the setting in all ten files that carried it. Note the fixed
  21 was already unreachable in practice, since the fetch itself only went
  back a fortnight -- so this is not a behaviour change so much as the two
  numbers finally agreeing.

The cutoff reads through getattr with the module default, because managers
are also built without __init__ -- the plugin test suites do exactly that,
and a bare attribute access raised into the surrounding except and silently
skipped the spoiler filter. Two existing tests caught that, which is the
only reason it is not in this commit as a regression.

_clamp_window also catches OverflowError now: json parses bare Infinity by
default and int(inf) raises, which the existing TypeError/ValueError guard
did not cover.

scripts/test_schedule_window_plumbing.py runs each of the nine adapters and
asserts both keys survive. Grepping for the key names would not have caught
this -- one plugin forwards its config with **self.config in a different
method, which reads like a passthrough. Mutation-checked: removing the
plumbing from the nine adapters fails all 18 checks.

soccer's test_schedule_horizon.py derived the full fetch's horizon from
timedelta literals to prove the partial was not narrower. Those literals
are gone, so it now asserts the stronger property that both fetches read
the same configured values and neither carries a hard-coded window.

All 95 test files across the nine plugins pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STMbQE4YctTacQXfbYqKuW
@ChuckBuilds
ChuckBuilds force-pushed the fix/schedule-window-plumbing branch from 2da6c51 to aa8bae6 Compare August 18, 2026 22:32
@ChuckBuilds
ChuckBuilds merged commit 081f9a3 into main Aug 19, 2026
4 checks passed
@ChuckBuilds
ChuckBuilds deleted the fix/schedule-window-plumbing branch August 19, 2026 00:02
ChuckBuilds pushed a commit that referenced this pull request Aug 19, 2026
ChuckBuilds added a commit that referenced this pull request Aug 19, 2026
* feat(sports): back off the live poll when a league has nothing on

Rebases #287 onto main and fixes the three things that stopped it working.
#287 stacked on #286, whose content reached main through #293 in a fixed
form, so only its own commit is carried here; the code applied cleanly and
the only conflicts were manifest version numbers and the generated registry.

The two settings could not reach the code. Each plugin's
_adapt_config_for_manager builds its output key by key, so a key it does not
name is dropped -- the same defect that made the schedule window inert, and
#293 fixed that by naming those two keys in a loop. Rather than adding two
more names to nine copies of a hard-coded tuple, the loop now walks a
module-level _ROOT_CONFIG_KEYS listing every plugin-root setting SportsCore
reads, so the next one only has to be declared once. Verified by running all
nine adapters: with all four keys set, all four now arrive at the config
root.

The ceiling did not bound the base interval. _idle_live_interval ended with
a bare `return base`, and the two settings are independent integers with no
cross-validation, so base > ceiling is a reachable config -- base=3600 with
the default 900 ceiling waited 3600s at streak 0 and 900s at streak 24. The
interval shrank as the streak grew, which is the opposite of what a setting
named "maximum" promises. Now min(base, ceiling), and checked across three
base/ceiling combinations that the sequence never decreases.

The test for that escalation could not fail: it compared long_wait to a
second call of the same method with the same state. Compared against the
short-streak value now. Mutation-checked -- making the long branch behave
like the short one is caught, where before it was not.

The plumbing test from #293 covers all four keys rather than two: removing
the loop from the nine adapters fails 36 of 36 checks.

All 104 test files across the nine plugins pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STMbQE4YctTacQXfbYqKuW

* fix(sports): read the idle-poll settings from where they are written

Review follow-up on the back-off change. Five findings, four real.

The settings were read from self.mode_config -- the per-league
{sport_key}_scoreboard block -- while the schema declares them, and the web
UI writes them, at the config root. Nothing ever wrote them where the code
looked, so a user who changed either value silently kept the default and the
whole setting was inert. The plumbing added alongside it carried the keys to
the root, and the test asserted they arrived there, but neither established
that the root was where they were read from: the wrong invariant, verified
thoroughly. Now read from the root with mode_config kept as a fallback, so a
hand-placed per-league value still works.

Also:

- _clamp_seconds now catches OverflowError, the gap _clamp_window in the same
  file already covered. json accepts bare Infinity by default and int(inf)
  raises, so a hand-edited config could take down the update loop.

- soccer's custom leagues go through a second whitelist adapter that was left
  out of the forwarding, so these settings applied to every built-in league
  but silently not to custom ones. Same defect class, second instance.

- the back-off test built a stand-in object and set the intervals on it by
  hand, so it could not see the config lookup at all -- which is exactly how
  the defect above survived it. It now constructs a real SportsLive from a
  real config dict and asserts on what __init__ resolved. Only
  SportsCore.__init__ is stubbed (it pulls in logo downloading, fonts and an
  ESPN data source); the lookup lines themselves run for real, and the stub's
  two assumptions are asserted against the real SportsCore AST so they cannot
  drift unnoticed.

  That guard earned itself immediately: ufc-scoreboard keys its league block
  on the bare sport_key rather than {sport_key}_scoreboard, so a hardcoded
  key made the test assert fiction there. The key is now read out of the
  source instead.

Verified by mutation, per plugin: reverting the read to mode_config fails 2
checks, letting a stale per-league value shadow the root fails 1, dropping
OverflowError raises, removing the custom-league forwarding fails 4, and
either stub assumption breaking fails its own check. The two pre-existing
baseball failures (test_config_reload, test_odds_placement) are byte-identical
on main and untouched here.

---------

Co-authored-by: Claude <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