Skip to content

fix(sports): stop rendering event start times in UTC (baseball + 9 plugins) - #228

Merged
ChuckBuilds merged 4 commits into
mainfrom
claude/baseball-plugin-timezone-nx7cka
Jul 29, 2026
Merged

fix(sports): stop rendering event start times in UTC (baseball + 9 plugins)#228
ChuckBuilds merged 4 commits into
mainfrom
claude/baseball-plugin-timezone-nx7cka

Conversation

@ChuckBuilds

@ChuckBuilds ChuckBuilds commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Pull Request

Summary

Every sports scoreboard rendered event start times in UTC (a 6:45pm Central first pitch showed as 11:45PM) even on setups where the clock plugin displayed the correct local time. All ten looked for the LEDMatrix global timezone only on cache_manager.config_manager; on cores that expose config_manager via the plugin manager instead, that lookup came back empty and resolution fell through to UTC. This centralizes timezone resolution per plugin, adds the missing fallbacks, and makes the timezone setting an actual, documented config option.

Originally scoped to baseball-scoreboard; extended after an audit found the identical defect in nine more plugins.

Type of change

  • Bug fix in an existing plugin
  • New plugin (also fill out the SUBMISSION.md checklist below)
  • New feature for an existing plugin
  • Documentation only
  • Repo-wide change (registry script, hook, top-level docs)

Plugin(s) affected

Plugin Version
baseball-scoreboard 1.19.3 → 1.20.0
football-scoreboard 2.8.3 → 2.9.0
hockey-scoreboard 1.4.1 → 1.5.0
basketball-scoreboard 1.7.1 → 1.8.0
soccer-scoreboard 2.3.1 → 2.4.0
ufc-scoreboard 1.2.4 → 1.3.0
lacrosse-scoreboard 1.4.1 → 1.5.0
nrl-scoreboard 1.0.4 → 1.1.0
afl-scoreboard 1.0.2 → 1.1.0
f1-scoreboard 1.5.1 → 1.6.0

Related issues

N/A — reported directly.

What changed

A plugin-prefixed resolver module per plugin (baseball_timezone.py, football_timezone.py, …) — separate copies rather than one shared module, because the core loads plugin top-level modules as bare names and namespace-isolates them afterward. Each is the single source of truth for its plugin's render paths and manager. First valid value wins:

  1. timezone in the plugin's own config (explicit override)
  2. plugin_manager.config_managerthe lookup that was missing everywhere; clock-simple and geochron check it first, which is why they were correct on the same device
  3. cache_manager.config_manager (what these plugins checked exclusively)
  4. Host system zone — TZ, /etc/timezone, /etc/localtime
  5. UTC

Candidates are evaluated lazily, so the per-event path stops at the first valid source instead of probing the filesystem on every render. Also supports older cores exposing load_config()/get_config() rather than get_timezone(), skips blank/invalid zone names with a warning, and never propagates an exception out of a core config manager.

Two related fixes make an explicit override actually usable:

  • config_schema.json never declared timezone, and seven of the ten set additionalProperties: false — so hand-editing the key in the saved config was discarded. It is now a string property under Advanced Settings, defaulting to "", with x-propertyOrder kept in sync where the schema defines one.
  • baseball and football mutated the config dict the core handed them (self.config["timezone"] = ...), persisting a bogus "timezone": "UTC" into the user's saved config which then shadowed the real global timezone. Both now keep the resolved value on the instance.

Left alone deliberately: march-madness (hardcodes US/Eastern for tip-off times), olympics (already declares its own timezone), ledmatrix-weather (uses the forecast location's zone, not the global config), and clock-simple/geochron/7-segment-clock/calendar/odds-ticker (already check plugin_manager first).

Test plan

  • Loaded the plugin in LEDMatrix on real hardware
  • Loaded the plugin in LEDMatrix emulator mode (EMULATOR=true python3 run.py)
  • Rendered the plugin in the dev preview server (scripts/dev_server.py)
  • Verified the web UI configuration form against the schema
  • Other — see below

test_timezone_resolution.py in each of the ten plugins (11 cases each, 110 total, all passing): resolution order, the plugin-level override winning, the plugin_manager lookup that was missing, lazy evaluation (asserts lower-priority sources go untouched once a candidate resolves), the legacy load_config() shape, a get_timezone() that raises, blank/invalid values being skipped, the system-zone backstop, the UTC last resort, and an end-to-end conversion of the reported 23:45Z → 6:45PM case.

Also run:

  • python scripts/check_module_collisions.py — OK across 41 plugins
  • Every test_*.py in all ten plugins, before and after. The seven that fail (football ×3, hockey ×2, basketball ×2, plus four in baseball) fail identically on main — they need the core src package and font assets not present in this environment. No regressions; also not a useful signal.
  • All touched config_schema.json / manifest.json re-parsed as JSON; schema diffs are purely additive.

Not verified on hardware or in the emulator — worth a sanity check on a real panel before release.

Required for plugin changes

  • Bumped version in plugins/<id>/manifest.json for all ten, each with a versions[] changelog entry
  • class_name in manifest.json matches the actual class in manager.py exactly (unchanged)
  • entry_point matches the real file (unchanged)
  • Updated the plugin's README.md if config keys changed — all ten document timezone; those with a Troubleshooting section also get an entry for the UTC symptom
  • config_schema.json is the source of truth for the web UI form — timezone added with default, description and x-advanced
  • Pre-commit hook ran successfully (auto-synced plugins.json)

Checklist

  • My commits follow the message convention in CONTRIBUTING.md
  • I read CONTRIBUTING.md and CODE_OF_CONDUCT.md
  • I've not committed any secrets

Notes for reviewer

Existing installs of baseball and football need one manual step. Anyone who ran an affected version has "timezone": "UTC" written into their saved config by the old write-back bug. Since an explicit plugin-level value wins, that stale "UTC" keeps overriding the fix until it's cleared or corrected. It's now editable in the web UI under Advanced Settings, and the READMEs call it out. I deliberately avoided auto-healing a stored "UTC" — there's no way to distinguish it from a deliberate choice, and silently overriding it would break users who genuinely want UTC.

Why nine copies of the same module rather than one shared file: the loader's bare-name import plus post-load namespace isolation makes a shared name risky for anything imported from a deferred or subpackage position, and scripts/check_module_collisions.py enforces plugin-unique names for exactly this reason. The duplication is deliberate and matches how these plugins already ship their own sports.py, scroll_display.py, etc.

The mechanical edits (resolver module, schema property, version bump, test) were script-generated from the baseball original and then verified per plugin; the non-uniform bits — hockey's second _get_timezone in base_classes.py, soccer's two manager call sites, f1's different _resolve_timezone signature, football's write-back — were handled individually.

The plugin looked for the LEDMatrix global timezone only on
cache_manager.config_manager. On cores that expose config_manager via the
plugin manager instead, that lookup came back empty and every start time was
drawn in UTC -- a 6:45pm Central first pitch rendered as 11:45PM -- while
clock-simple and geochron, which check plugin_manager first, showed the correct
local time on the same device.

Timezone resolution now lives in one place (baseball_timezone.py) shared by the
switch-mode scorebug, the scroll-mode game card and the plugin manager. Order:
plugin override, plugin_manager.config_manager, cache_manager.config_manager,
host system zone (TZ, /etc/timezone, /etc/localtime), then UTC.

Two related fixes make an explicit override actually usable:

- config_schema.json never declared `timezone` and sets
  additionalProperties: false, so hand-editing the key in the saved config had
  no effect. It is now a documented string property under Advanced Settings.
- The manager assigned self.config["timezone"], mutating the dict the core
  handed it and persisting a bogus "timezone": "UTC" into the user's saved
  plugin config. The resolved value is kept on the instance and passed to
  sub-components via a copy instead.

Adds test_timezone_resolution.py covering the resolution order, invalid and
blank values, a raising config_manager, and the system-zone backstop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QSnspNZceRCdpUtJh2Co6e
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@ChuckBuilds, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 55 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2ccf146b-c058-4519-b99a-3615376f1229

📥 Commits

Reviewing files that changed from the base of the PR and between 92329f9 and 5e1bce3.

📒 Files selected for processing (68)
  • plugins.json
  • plugins/afl-scoreboard/README.md
  • plugins/afl-scoreboard/afl_timezone.py
  • plugins/afl-scoreboard/config_schema.json
  • plugins/afl-scoreboard/manager.py
  • plugins/afl-scoreboard/manifest.json
  • plugins/afl-scoreboard/sports.py
  • plugins/afl-scoreboard/test_timezone_resolution.py
  • plugins/baseball-scoreboard/baseball_timezone.py
  • plugins/baseball-scoreboard/test_timezone_resolution.py
  • plugins/basketball-scoreboard/README.md
  • plugins/basketball-scoreboard/basketball_timezone.py
  • plugins/basketball-scoreboard/config_schema.json
  • plugins/basketball-scoreboard/manager.py
  • plugins/basketball-scoreboard/manifest.json
  • plugins/basketball-scoreboard/sports.py
  • plugins/basketball-scoreboard/test_timezone_resolution.py
  • plugins/f1-scoreboard/README.md
  • plugins/f1-scoreboard/config_schema.json
  • plugins/f1-scoreboard/f1_timezone.py
  • plugins/f1-scoreboard/manager.py
  • plugins/f1-scoreboard/manifest.json
  • plugins/f1-scoreboard/test_timezone_resolution.py
  • plugins/football-scoreboard/CHANGELOG.md
  • plugins/football-scoreboard/README.md
  • plugins/football-scoreboard/config_schema.json
  • plugins/football-scoreboard/football_timezone.py
  • plugins/football-scoreboard/manager.py
  • plugins/football-scoreboard/manifest.json
  • plugins/football-scoreboard/sports.py
  • plugins/football-scoreboard/test_timezone_resolution.py
  • plugins/hockey-scoreboard/README.md
  • plugins/hockey-scoreboard/base_classes.py
  • plugins/hockey-scoreboard/config_schema.json
  • plugins/hockey-scoreboard/hockey_timezone.py
  • plugins/hockey-scoreboard/manager.py
  • plugins/hockey-scoreboard/manifest.json
  • plugins/hockey-scoreboard/sports.py
  • plugins/hockey-scoreboard/test_timezone_resolution.py
  • plugins/lacrosse-scoreboard/CHANGELOG.md
  • plugins/lacrosse-scoreboard/README.md
  • plugins/lacrosse-scoreboard/config_schema.json
  • plugins/lacrosse-scoreboard/lacrosse_timezone.py
  • plugins/lacrosse-scoreboard/manager.py
  • plugins/lacrosse-scoreboard/manifest.json
  • plugins/lacrosse-scoreboard/sports.py
  • plugins/lacrosse-scoreboard/test_timezone_resolution.py
  • plugins/nrl-scoreboard/README.md
  • plugins/nrl-scoreboard/config_schema.json
  • plugins/nrl-scoreboard/manager.py
  • plugins/nrl-scoreboard/manifest.json
  • plugins/nrl-scoreboard/nrl_timezone.py
  • plugins/nrl-scoreboard/sports.py
  • plugins/nrl-scoreboard/test_timezone_resolution.py
  • plugins/soccer-scoreboard/README.md
  • plugins/soccer-scoreboard/config_schema.json
  • plugins/soccer-scoreboard/manager.py
  • plugins/soccer-scoreboard/manifest.json
  • plugins/soccer-scoreboard/soccer_timezone.py
  • plugins/soccer-scoreboard/sports.py
  • plugins/soccer-scoreboard/test_timezone_resolution.py
  • plugins/ufc-scoreboard/README.md
  • plugins/ufc-scoreboard/config_schema.json
  • plugins/ufc-scoreboard/manager.py
  • plugins/ufc-scoreboard/manifest.json
  • plugins/ufc-scoreboard/sports.py
  • plugins/ufc-scoreboard/test_timezone_resolution.py
  • plugins/ufc-scoreboard/ufc_timezone.py
📝 Walkthrough

Walkthrough

Baseball Scoreboard 1.20.0 centralizes timezone resolution, adds an advanced timezone configuration property, updates manager and rendering paths to use resolved timezones without mutating saved configuration, adds regression tests, and documents the release.

Changes

Baseball Scoreboard timezone handling

Layer / File(s) Summary
Centralized timezone resolution and validation
plugins/baseball-scoreboard/baseball_timezone.py, plugins/baseball-scoreboard/test_timezone_resolution.py
Adds ordered timezone resolution across plugin configuration, config managers, host sources, and UTC, with validation, legacy config support, exception handling, and regression tests.
Runtime configuration and rendering integration
plugins/baseball-scoreboard/config_schema.json, plugins/baseball-scoreboard/manager.py, plugins/baseball-scoreboard/game_renderer.py, plugins/baseball-scoreboard/sports.py
Adds the advanced timezone schema property, stores resolved values separately from persisted configuration, passes them to subcomponents, and uses shared resolution for game-time rendering.
Version metadata and configuration documentation
plugins.json, plugins/baseball-scoreboard/CHANGELOG.md, plugins/baseball-scoreboard/README.md, plugins/baseball-scoreboard/manifest.json
Updates version metadata to 1.20.0 and documents timezone settings, fallback behavior, troubleshooting, and release changes.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Manager
  participant TimezoneResolver
  participant ConfigManagers
  participant Renderer
  Manager->>TimezoneResolver: Resolve effective timezone
  TimezoneResolver->>ConfigManagers: Check plugin and core configuration
  ConfigManagers-->>TimezoneResolver: Return candidate timezone
  TimezoneResolver-->>Manager: Return validated timezone
  Manager->>Renderer: Pass resolved timezone configuration
  Renderer->>TimezoneResolver: Resolve timezone for game start
  TimezoneResolver-->>Renderer: Return tzinfo
  Renderer->>Renderer: Convert UTC start time to local time
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.46% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title captures the core UTC rendering fix, but it overstates the scope with “baseball + 9 plugins.”
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/baseball-plugin-timezone-nx7cka

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 Jul 28, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 468 complexity

Metric Results
Complexity 468

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@plugins/baseball-scoreboard/baseball_timezone.py`:
- Around line 107-118: Update the timezone candidate resolution around the
candidates collection and its consuming loop to evaluate sources lazily,
yielding each candidate only when reached. Preserve the existing priority
order—plugin config, plugin-manager config, cache-manager config, then system
timezone—and stop evaluating lower-priority sources once a valid timezone is
found.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7a6255e8-da3a-4f12-b821-8e77eee6962a

📥 Commits

Reviewing files that changed from the base of the PR and between 3f7c90e and 92329f9.

📒 Files selected for processing (10)
  • plugins.json
  • plugins/baseball-scoreboard/CHANGELOG.md
  • plugins/baseball-scoreboard/README.md
  • plugins/baseball-scoreboard/baseball_timezone.py
  • plugins/baseball-scoreboard/config_schema.json
  • plugins/baseball-scoreboard/game_renderer.py
  • plugins/baseball-scoreboard/manager.py
  • plugins/baseball-scoreboard/manifest.json
  • plugins/baseball-scoreboard/sports.py
  • plugins/baseball-scoreboard/test_timezone_resolution.py

Comment thread plugins/baseball-scoreboard/baseball_timezone.py Outdated
resolve_timezone_name() built its candidate list eagerly, so every call
queried both config managers and stat'd the host timezone files even when the
plugin config already supplied a valid zone. SportsCore._get_timezone() runs
once per game during extraction, making that a per-game filesystem probe.

Yield candidates from a generator instead, preserving the priority order, and
assert in the tests that lower-priority sources are left untouched once a
candidate resolves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QSnspNZceRCdpUtJh2Co6e
Ports the baseball-scoreboard timezone fix to every other plugin carrying the
same defect. All nine looked for the LEDMatrix global timezone only on
cache_manager.config_manager; on cores that expose config_manager via the
plugin manager instead, that lookup came back empty and every start time was
drawn in UTC, while clock-simple and geochron -- which check plugin_manager
first -- showed the correct local time on the same device.

Each plugin gets its own plugin-prefixed resolver module (football_timezone.py,
hockey_timezone.py, ...) rather than a shared one, since the core loads plugin
top-level modules as bare names. Resolution order: plugin override,
plugin_manager.config_manager, cache_manager.config_manager, host system zone
(TZ, /etc/timezone, /etc/localtime), then UTC. Candidates are evaluated lazily
so the per-event path stops at the first valid source.

Also, in every plugin:

- config_schema.json never declared `timezone`, and seven of the nine set
  additionalProperties: false, so hand-editing the key was discarded. It is
  now a documented string property under Advanced Settings, with
  x-propertyOrder kept in sync where the schema defines one.
- football-scoreboard additionally assigned self.config["timezone"] in
  on_config_change, mutating the core's dict and persisting a stale
  "timezone": "UTC" that then shadowed the real global timezone. Removed.

Versions bumped (minor -- new config property): football 2.9.0, hockey 1.5.0,
basketball 1.8.0, soccer 2.4.0, ufc 1.3.0, lacrosse 1.5.0, nrl 1.1.0,
afl 1.1.0, f1 1.6.0.

Each plugin gets test_timezone_resolution.py (11 cases) covering the
resolution order, lazy evaluation, invalid and blank values, a raising
config_manager, and the system-zone backstop. Module-collision check passes;
the pre-existing test failures in these plugins are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QSnspNZceRCdpUtJh2Co6e
@ChuckBuilds ChuckBuilds changed the title fix(baseball-scoreboard): stop rendering game start times in UTC fix(sports): stop rendering event start times in UTC (baseball + 9 plugins) Jul 29, 2026
… name

The safety harness went red on ufc-scoreboard with
ModuleNotFoundError("No module named 'logo_downloader'") at plugin load.

Pre-existing, not introduced here: sports.py imported the bare name
`logo_downloader`, but ufc-scoreboard ships no such module. The core loads
plugin top-level .py files as bare-name modules on sys.path, so the import only
ever succeeded by binding a *different* plugin's logo_downloader -- exactly the
cross-plugin coupling scripts/check_module_collisions.py exists to prevent --
and raised outright when no such plugin happened to be loaded. The harness only
runs *changed* plugins, so it had never exercised ufc-scoreboard before this
branch touched it.

Now imports src.logo_downloader from the core, matching the soccer, nrl and afl
plugins and this same file's existing deferred `from src.logo_downloader import
LogoDownloader` a few lines below, including their sys.path preamble.

Verified sports.py and mma.py import cleanly against a stub src.logo_downloader
and that LogoDownloader now resolves to src.logo_downloader. Folded into the
unreleased 1.3.0 notes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QSnspNZceRCdpUtJh2Co6e
@ChuckBuilds
ChuckBuilds merged commit 8d33894 into main Jul 29, 2026
4 checks passed
ChuckBuilds pushed a commit that referenced this pull request Jul 30, 2026
…mes release

Both branches bumped soccer-scoreboard to 2.4.0 — main's is the UTC start-time
fix (#228). Rebased this work onto 2.5.0 so main's release notes are preserved
beneath it rather than replaced.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
ChuckBuilds pushed a commit that referenced this pull request Jul 30, 2026
…-times release

#228 landed and also bumped baseball-scoreboard, so both branches claimed 1.20.0.
This work is now 1.20.1 with main's release notes preserved beneath it rather
than replaced. manager.py and config_schema.json auto-merged; verified the merged
manager.py compiles and carries both changes (baseball_timezone from #228 and the
Vegas slate work here). Harness passes all 24 screens post-merge.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
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