feat(sports): make the schedule fetch window configurable - #286
feat(sports): make the schedule fetch window configurable#286ChuckBuilds wants to merge 2 commits into
Conversation
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
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (29)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughNine scoreboard plugins now support configurable schedule lookback and lookahead windows. Values are validated within 1–60 days, with plugin-specific defaults. Schemas, runtime fetching, manifests, catalog versions, and soccer regression coverage are updated. ChangesScoreboard schedule horizons
Estimated code review effort: 3 (Moderate) | ~25 minutes Merge Risk: 🔵 Low · up to The change makes schedule windows configurable with bounded fallback behavior and widens soccer’s default lookahead; it is mergeable with owner awareness that the new horizon test’s shared module import could bind the wrong scoreboard module in some test-process arrangements, weakening protection against regressions. Sequence Diagram(s)sequenceDiagram
participant Configuration
participant SportsCore
participant ScoreboardAPI
Configuration->>SportsCore: provide schedule window settings
SportsCore->>SportsCore: clamp or default values
SportsCore->>ScoreboardAPI: request the resolved schedule date range
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
⚔️ Resolve merge conflicts 💡
🧪 Generate unit tests (beta)
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 reviewer
TIP This summary will be updated as you push new changes.
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
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
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
…293) * fix(soccer): look two weeks ahead for fixtures, matching the real fetch 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 * feat(sports): make the schedule fetch window configurable 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 * fix(sports): make the schedule-window settings actually take effect 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 --------- Co-authored-by: Claude <noreply@anthropic.com>
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
Superseded by #293 ( #293 carried this branch's work rebased onto
Resolving the conflicts here would not add anything. I ran the merge against current |
* 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>
Stacked on #285 — merge that first.
What this adds
schedule_lookback_daysandschedule_lookahead_days, advanced settings on all nine scoreboards. Previously hard-coded at 14 back / 7 forward.Bounded 1–60 days. A non-numeric or out-of-range value falls back to the default rather than raising, so a bad config can't take the plugin down or turn one refresh into a season-wide request.
Defaults keep today's behaviour, so no board changes on upgrade. The one exception is soccer, whose lookahead moves 7 → 14 to match the fetch its partial substitutes for — that's the bug from #285.
Why days is the wrong primitive
Worth stating plainly, because this is an escape hatch rather than the answer.
"Show me the next 5 games" is a count. The days it spans depends entirely on the league's cadence — about 5 days for MLB, about 5 weeks for the Premier League. So any fixed number of days is wrong for one of them, and this setting makes the user do that arithmetic themselves.
The better version sets the horizon from a game count and expands until it's satisfied, gated on whether favourite teams are configured:
Cost: unchanged for no-favourites and for daily leagues (satisfied at the first step); 2–3 requests on a cold cache for a weekly league, which is the case that's currently broken. Future fixtures are static, so a long cache TTL means that's paid once.
The plugin already resolves
self.favorite_teamsinSportsCore.__init__, before the first fetch, so the gate is available where it's needed.Verification
x-advanced: true, bounds 1–60.soccer_managers.pyand requires the configured default to be at least as wide, so the two can't drift apart again.srcones that also fail onmain.Separate finding, not fixed here
Measuring API load on a live rig turned up idle-season polling:
NHLLiveManagerfetched 0 games 22 times in 2 hours — every ~5.5 minutes, year-round, mid-August. Roughly 264 requests/day for one out-of-season league, all returning nothing. That's a defaults problem rather than a configurability one and deserves its own change.Summary by CodeRabbit