Skip to content

fix(nrl-scoreboard): match favorite/exclude teams by ESPN team ID, not abbreviation - #189

Merged
ChuckBuilds merged 3 commits into
mainfrom
claude/nrl-team-ids-abbrev-lepchm
Jul 16, 2026
Merged

fix(nrl-scoreboard): match favorite/exclude teams by ESPN team ID, not abbreviation#189
ChuckBuilds merged 3 commits into
mainfrom
claude/nrl-team-ids-abbrev-lepchm

Conversation

@ChuckBuilds

@ChuckBuilds ChuckBuilds commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes a bug reported by a user testing PR #182 (NRL scoreboard):

One thing I found with the NRL is that you need to use team id and not abbreviations - there are two "NEW" and two "CAN"

ESPN's rugby-league team abbreviations are not unique: NEW is used by both the Newcastle Knights and the New Zealand Warriors, and CAN is used by both the Canberra Raiders and the Canterbury Bulldogs. favorite_teams/exclude_teams matching previously compared against home_abbr/away_abbr, so configuring one of these abbreviations silently matched both teams.

Fix

  • dynamic_team_resolver.py: DynamicTeamResolver now fetches the NRL team list from ESPN once (in-process cache, 24h TTL) and resolves each configured favorite_teams/exclude_teams entry to a unique ESPN team ID:
    • Numeric entries pass through as-is (already an ID).
    • Full team name / location / nickname / short name matches resolve unambiguously.
    • A unique abbreviation still resolves.
    • An ambiguous abbreviation (e.g. NEW, CAN) is not silently resolved to either team — it logs an error naming both candidate teams and instructs the user to use the full team name or ID instead.
    • If the team list can't be fetched (offline, ESPN outage) it falls back to a stale cache, then to the raw configured value, with a warning.
  • sports.py / manager.py: every favorite/exclude membership check now compares home_id/away_id (already extracted per-game) against the resolved team-ID list, instead of home_abbr/away_abbr. This covers live rotation filtering/classification, live-priority weighting, goal/win celebrations, and upcoming/recent game selection and filtering.
  • config_schema.json / README.md: updated descriptions to steer users toward full team names or ESPN team IDs, with the NEW/CAN collision called out explicitly.
  • test/harness.json: favorite_teams switched from ["PEN", "BRI"] to the corresponding team IDs (["18", "16"]) so the offline test harness doesn't depend on a live network fetch to resolve favorites.
  • test_nrl_plugin.py: added TestDynamicTeamResolverAbbreviationCollisions covering the ambiguous-abbreviation case, full-name disambiguation, unique-abbreviation resolution, numeric ID passthrough, and fetch-failure fallback (mocks requests.get, no network needed).

Test plan

  • python3 -m py_compile all touched .py files — passes.
  • python3 test_nrl_plugin.py — 11/11 tests pass (6 pre-existing + 5 new).
  • manifest.json, config_schema.json, test/harness.json all parse.
  • python scripts/check_module_collisions.py — no cross-plugin collisions.
  • python update_registry.py — registry synced, version bumped to 1.0.1.
  • Not run: the core check_plugin.py cross-size harness (requires a LEDMatrix core checkout — none available in this environment).
  • Not run: live on-device verification against the real ESPN team-list endpoint (this sandbox's egress policy blocks site.api.espn.com).

🤖 Generated with Claude Code


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • NRL team preferences now support full team names and ESPN team IDs, with safer handling of ambiguous abbreviations.
    • Favorite and excluded teams are matched consistently across upcoming, recent, and live games.
    • Team selections are automatically resolved and cached for improved reliability.
  • Bug Fixes

    • Prevented shared abbreviations from selecting the wrong team.
    • Updated live-game prioritization and celebrations to use unique team identifiers.
  • Documentation

    • Clarified accepted team formats and updated configuration examples.
  • Chores

    • Updated the NRL Scoreboard plugin version to 1.0.2.

@coderabbitai

coderabbitai Bot commented Jul 12, 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: 40 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

Run ID: 972ee628-0006-40f7-b994-9f00b421460b

📥 Commits

Reviewing files that changed from the base of the PR and between 8c5aafe and 0030f5c.

📒 Files selected for processing (7)
  • plugins.json
  • plugins/nrl-scoreboard/README.md
  • plugins/nrl-scoreboard/config_schema.json
  • plugins/nrl-scoreboard/dynamic_team_resolver.py
  • plugins/nrl-scoreboard/manager.py
  • plugins/nrl-scoreboard/manifest.json
  • plugins/nrl-scoreboard/sports.py
📝 Walkthrough

Walkthrough

Changes

NRL team ID matching

Layer / File(s) Summary
Team resolver and cache
plugins/nrl-scoreboard/dynamic_team_resolver.py
Fetches and caches ESPN NRL teams, resolves names and abbreviations to IDs, preserves numeric IDs, and falls back on unresolved values.
ID-based scoreboard matching
plugins/nrl-scoreboard/manager.py, plugins/nrl-scoreboard/sports.py
Favorite, excluded, display-selection, live-classification, and celebration checks now use ESPN team IDs.
Configuration, release, and validation
plugins/nrl-scoreboard/README.md, plugins/nrl-scoreboard/config_schema.json, plugins/nrl-scoreboard/manifest.json, plugins/nrl-scoreboard/test/*, plugins/nrl-scoreboard/test_nrl_plugin.py
Documents full names and IDs, updates the release metadata and fixtures, and tests ambiguous abbreviation handling and resolver fallbacks.

Plugin metadata updates

Layer / File(s) Summary
Catalog metadata
plugins.json
Updates two plugin descriptions and bumps the AFL scoreboard latest version to 1.0.2.

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

Sequence Diagram(s)

sequenceDiagram
  participant PluginConfig
  participant DynamicTeamResolver
  participant ESPN
  participant SportsLive
  PluginConfig->>DynamicTeamResolver: resolve configured team values
  DynamicTeamResolver->>ESPN: request NRL teams
  ESPN-->>DynamicTeamResolver: team names, abbreviations, and IDs
  DynamicTeamResolver-->>PluginConfig: resolved team IDs
  PluginConfig->>SportsLive: provide favorite team IDs
  SportsLive-->>PluginConfig: classify games and gate celebrations by team ID
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: switching NRL favorite/exclude team matching from abbreviations to ESPN team IDs.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/nrl-team-ids-abbrev-lepchm

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 12, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 33 complexity

Metric Results
Complexity 33

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.

claude added 2 commits July 15, 2026 17:39
…t abbreviation

NRL's ESPN abbreviations aren't unique — "NEW" is both Newcastle Knights and
New Zealand Warriors, "CAN" is both Canberra Raiders and Canterbury Bulldogs.
Matching favorite_teams/exclude_teams by abbreviation silently conflated
these teams. DynamicTeamResolver now resolves configured team names/IDs to
unique ESPN team IDs (fetching the NRL team list, with a name/ID lookup and
an explicit warning on ambiguous or unresolved abbreviations), and every
favorite/exclude membership check in sports.py and manager.py now compares
against home_id/away_id instead of home_abbr/away_abbr.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rxy8kp5foWywPeBsCmY1Uo
The plugin safety harness (LEDMatrix core's check_plugin.py) expects
mock_data in test/harness.json to be a path string to a fixture file, not
inline JSON - it does Path(plugin_dir) / mock_data, which raises TypeError
against a dict. Every other plugin harness in this repo follows the
path convention (e.g. ledmatrix-weather's test/fixtures/mock.json); this
plugin's harness.json had the ESPN response inlined instead, which was
failing the "safety" CI check on both this PR and #182.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rxy8kp5foWywPeBsCmY1Uo
@ChuckBuilds
ChuckBuilds changed the base branch from claude/nrl-scoreboard to main July 16, 2026 01:23
@ChuckBuilds
ChuckBuilds force-pushed the claude/nrl-team-ids-abbrev-lepchm branch from c3ae9d3 to 8c5aafe Compare July 16, 2026 01:24

@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: 2

🧹 Nitpick comments (2)
plugins/nrl-scoreboard/dynamic_team_resolver.py (1)

69-77: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Simplify list deduplication.

You can replace this manual deduplication loop with dict.fromkeys(), which preserves order and is more concise.

♻️ Proposed refactor
-        # Remove duplicates while preserving order
-        seen = set()
-        unique_teams = []
-        for team in resolved_teams:
-            if team not in seen:
-                seen.add(team)
-                unique_teams.append(team)
-
-        return unique_teams
+        # Remove duplicates while preserving order
+        return list(dict.fromkeys(resolved_teams))
🤖 Prompt for 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.

In `@plugins/nrl-scoreboard/dynamic_team_resolver.py` around lines 69 - 77,
Replace the manual deduplication loop using seen and unique_teams in the
resolver method with an order-preserving dict.fromkeys() expression over
resolved_teams, while returning the same list result.
plugins/nrl-scoreboard/sports.py (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated str(id) in team_list matching logic across ~9 sites. The same "is this game a favorite/excluded" ID-membership check is re-implemented independently in sports.py (_is_favorite_game, _classify_live_game, _extract_game_details_common, _select_games_for_display, SportsUpcoming.update, _select_recent_games_for_display, SportsRecent.update, _is_favorite) and in manager.py. Extracting one shared helper (e.g. a SportsCore static method _team_in(team_id, team_list)) would reduce the risk of future edits drifting inconsistently between call sites.

  • plugins/nrl-scoreboard/sports.py#L185-189: introduce and use a shared _team_in/_any_team_in helper here as the canonical implementation.
  • plugins/nrl-scoreboard/manager.py#L663-676: reuse the same helper (via the manager or a shared utility) instead of re-deriving the membership check inline.
♻️ Example helper
+    `@staticmethod`
+    def _team_in(team_id, team_list) -> bool:
+        return bool(team_list) and str(team_id) in team_list
+
     def _is_favorite_game(self, game: Dict) -> bool:
-        return bool(self.favorite_teams) and (
-            str(game.get("home_id")) in self.favorite_teams
-            or str(game.get("away_id")) in self.favorite_teams
-        )
+        return self._team_in(game.get("home_id"), self.favorite_teams) or self._team_in(
+            game.get("away_id"), self.favorite_teams
+        )
🤖 Prompt for 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.

In `@plugins/nrl-scoreboard/sports.py` at line 1, Extract the repeated team-ID
membership check into a canonical helper on SportsCore, such as _team_in or
_any_team_in, and replace the duplicated str(id) in team_list logic in the
listed sports.py methods. Update manager.py to reuse that same helper instead of
performing the check inline, preserving existing favorite/exclusion behavior.
🤖 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/nrl-scoreboard/dynamic_team_resolver.py`:
- Around line 173-175: Update the team ID extraction in the dynamic team
resolver to convert valid IDs from t.get("id") to strings before storing them in
the cache, while preserving the existing skip behavior for missing IDs. Ensure
favorite_teams uses string IDs consistently with downstream membership checks in
sports.py.

In `@plugins/nrl-scoreboard/README.md`:
- Around line 67-68: Update the favorite_teams documentation to clarify that
ambiguous abbreviations such as NEW and CAN are rejected or left unresolved by
the resolver, which logs an error, rather than matching both teams. Keep the
full team name or ESPN team ID guidance and the exclude_teams cross-reference
unchanged.

---

Nitpick comments:
In `@plugins/nrl-scoreboard/dynamic_team_resolver.py`:
- Around line 69-77: Replace the manual deduplication loop using seen and
unique_teams in the resolver method with an order-preserving dict.fromkeys()
expression over resolved_teams, while returning the same list result.

In `@plugins/nrl-scoreboard/sports.py`:
- Line 1: Extract the repeated team-ID membership check into a canonical helper
on SportsCore, such as _team_in or _any_team_in, and replace the duplicated
str(id) in team_list logic in the listed sports.py methods. Update manager.py to
reuse that same helper instead of performing the check inline, preserving
existing favorite/exclusion behavior.
🪄 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

Run ID: ce8dedba-5282-4d45-a1c0-6b9db89bdc9f

📥 Commits

Reviewing files that changed from the base of the PR and between 47ab45b and 8c5aafe.

📒 Files selected for processing (10)
  • plugins.json
  • plugins/nrl-scoreboard/README.md
  • plugins/nrl-scoreboard/config_schema.json
  • plugins/nrl-scoreboard/dynamic_team_resolver.py
  • plugins/nrl-scoreboard/manager.py
  • plugins/nrl-scoreboard/manifest.json
  • plugins/nrl-scoreboard/sports.py
  • plugins/nrl-scoreboard/test/fixtures/mock.json
  • plugins/nrl-scoreboard/test/harness.json
  • plugins/nrl-scoreboard/test_nrl_plugin.py

Comment thread plugins/nrl-scoreboard/dynamic_team_resolver.py
Comment thread plugins/nrl-scoreboard/README.md Outdated
…tching

- dynamic_team_resolver.py: simplify the dedup loop to dict.fromkeys();
  stringify ESPN team IDs when building the by_name/by_abbr lookup index so
  an integer ID from ESPN can't silently fail downstream str() comparisons
- sports.py: extract a shared SportsCore._team_in(team_id, team_list)
  helper and use it at every favorite/exclude membership check (previously
  ~9 independently-reimplemented `str(id) in team_list` sites across
  _is_favorite_game, _classify_live_game, _extract_game_details_common,
  SportsUpcoming.update, SportsRecent.update, _is_favorite)
- manager.py: reuse the same helper (via the live manager instance) in
  _has_favorite_or_all_live instead of re-deriving the check inline
- README.md, config_schema.json: fix wording - an ambiguous abbreviation
  (NEW, CAN) is left unresolved and logged as an error, not matched to
  either team

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rxy8kp5foWywPeBsCmY1Uo
@ChuckBuilds
ChuckBuilds merged commit e9429ac into main Jul 16, 2026
4 checks passed
@ChuckBuilds
ChuckBuilds deleted the claude/nrl-team-ids-abbrev-lepchm branch July 16, 2026 12:49
ChuckBuilds pushed a commit that referenced this pull request Aug 31, 2026
… this

Main moved a long way under this branch. Resolving hunk by hunk showed the
work splits cleanly in two, and the halves want opposite resolutions.

**football-scoreboard is fully superseded.** What this branch does for football
shipped as #335 and was then improved by #341, #343 and #344. Main is strictly
ahead everywhere they overlap: it season-corrects the division roster lookup,
str()-guards ESPN's broadcast field, catches OverflowError on a bare Infinity,
routes the no-favourites branch through _favorites_first so it builds selection
pools, and adds _attach_odds_to_rotated_games, which this branch does not have
at all. Several of main's comments describe this branch's own approach as the
old way. Every football hunk therefore takes main, and football's sports.py,
manager.py, data_sources.py, tests and README now match origin/main byte for
byte -- this branch no longer changes football in any way.

**The other eight lineages still need all of it.** They have _favorites_first
and nothing else: no _compose_selection, no rotation, no _passes_other_filters.
Their settings were in the schema with nothing reading them. Those hunks take
this branch.

Three resolutions worth naming:

- baseball, basketball, hockey and lacrosse: main re-adds _is_favorite_game
  beside the rotation methods this branch adds. Taking both would have defined
  it twice in one class, and Python takes the last -- main's copy would have
  silently shadowed this branch's. Kept one, plus main's _DRAWS_SCORE ClassVar.
- nrl: main defines _is_favorite_game TWICE in SportsCore, at 320 and 2177, and
  they do not agree -- the first matches on ESPN team id, the second on
  abbreviation. #189 moved nrl to ids deliberately; #332 added the abbr copy,
  which shadowed and silently reverted that fix. This branch removes the
  duplicate, so the fix is restored. That is a live bug on main today.
- Manifests and plugins.json take main, then the eight changed plugins are
  re-bumped on top of the versions main has since published. Football is not
  bumped, because nothing about it changed.

READMEs: football takes main; afl, baseball and basketball keep both sections,
since this branch documents selection and main documents the matchup separator.

Verified: run_plugin_tests.py --all gives 222 passed, 2 skipped, 1 failed, and
that one failure -- football's test_favorite_live_boost.py, "excluded team
hidden from recent/final scores in default (no-favorites) path" -- reproduces
identically on a clean origin/main worktree. It is main's, not this merge's:
_favorites_first(games, 0, N) does not apply the exclude filter the old
filter/sort/truncate path did. The eight apply excludes elsewhere and pass.
check_selection_settings, check_manifest_version_fields, check_scroll_adoption
and check_module_collisions all pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9
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