fix(masters-tournament): repoint to live ESPN endpoints, fix <1d countdown#94
fix(masters-tournament): repoint to live ESPN endpoints, fix <1d countdown#94ChuckBuilds merged 3 commits intomainfrom
Conversation
…n under 1 day
The plugin was falling back to mock data for the 2026 Masters because ESPN
retired the legacy /sports/golf/pga/leaderboard path (now 404). Countdown was
also hardcoded to Apr 10 12:00 UTC (2025 Masters Thursday) and appeared stuck
at ~23 hours all day on Apr 9 2026, the day the 2026 tournament actually began.
Endpoint fixes:
- LEADERBOARD_URL -> site.api.espn.com/.../sports/golf/leaderboard (no pga/)
- ATHLETE_URL -> site.web.api.espn.com/apis/common/v3/.../athletes/{id}
- ATHLETE_OVERVIEW_URL (new) for season stats + seasonRankings
- Drop dead /sports/golf/pga/schedule call; tee times come off leaderboard
Countdown + phase now date-driven from live ESPN feed:
- New MastersDataSource.fetch_tournament_meta() parses events[0].date,
endDate, status.type.state, and period from the leaderboard response and
caches them alongside it (one HTTP call serves leaderboard + meta + tee times)
- manager._display_countdown() uses meta.start_date instead of the hardcoded
April 10 target; falls back to computed second-Thursday-of-April off-season
- get_tournament_phase / get_detailed_phase in masters_helpers now accept
start_date/end_date and compute phase by comparison (hardcoded month/day
heuristic remains as a fallback)
- Cache TTL derived from meta: 30s during live play, 300s within 3 days of
start, 3600s otherwise
Leaderboard parser fixes (surfaced by the live endpoint shape):
- Position now read from status.position.displayName (was entry.position,
which is null in the current payload, yielding None in the position column)
- _calculate_score_to_par guards "-" placeholders (was calling int("-"))
- _extract_round_scores skips linescores with displayValue in (None, "", "-")
so pre-round 0.0 placeholders don't register as completed rounds of 0
- Country: normalize from flag.href path (.../countries/500/usa.png -> USA)
instead of dropping ESPN's "United States" alt to empty string; result is
91/91 players with a country code vs. ~25 before
- Headshot: final fallback constructs the ESPN CDN URL from player_id so
players missing from the hardcoded DB still get a portrait
Renderer: add sub-hour branch to render_countdown so the final hour shows
"{m}m MINUTES TO GO" instead of jumping straight from hours to "NOW".
Player details (was a stub returning None): fetch_player_details now hits the
athlete + overview endpoints and merges bio (age, height, weight, college,
birth place, flag, headshot) with statistics.splits and seasonRankings.categories.
Validated live against ESPN for the 2026 Masters on 2026-04-09:
- 91/91 players with id + country + headshot
- meta: name=Masters Tournament, status=in, round=1, start/end from ESPN
- 31 tee-time pairings covering all 91 players
- Scheffler player details: full bio + 6 stats + 9 season rankings
- Countdown renders NOW (tournament in progress); T+45m renders "44m MINUTES TO GO"
- get_tournament_phase / get_detailed_phase return tournament / tournament-live
- test_plugin_standalone.py: all green
Bumps manifest 2.0.0 -> 2.1.0.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds ESPN-derived tournament metadata caching, date-driven phase detection, and richer data fetching (leaderboard, schedule, player details) to the masters-tournament plugin; bumps plugin manifest to 2.1.2 and refines countdown rendering. Changes
Sequence DiagramsequenceDiagram
participant Manager as MastersTournamentPlugin
participant DataSource as masters_data
participant ESPN as ESPN API
participant Cache as Cache Layer
participant Helpers as masters_helpers
Manager->>DataSource: fetch_tournament_meta()
DataSource->>Cache: read tournament_meta
alt cached
Cache-->>DataSource: return meta
else not cached
DataSource->>ESPN: fetch leaderboard (and athlete endpoints as needed)
ESPN-->>DataSource: leaderboard payload (meta, players, teeTimes)
DataSource->>DataSource: parse meta, tee times, player ids/details
DataSource->>Cache: write tournament_meta, schedule, player details
Cache-->>DataSource: ack
end
DataSource-->>Manager: tournament_meta (start_date, end_date)
Manager->>Helpers: get_tournament_phase(start_date, end_date)
Helpers-->>Manager: phase
Manager->>Helpers: get_detailed_phase(start_date, end_date)
Helpers-->>Manager: detailed_phase
Manager->>Manager: update() interval -> fetch_tournament_meta() -> rebuild modes / render countdown
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 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 |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@plugins/masters-tournament/manifest.json`:
- Around line 46-50: The new entry in the manifest's versions array uses the
wrong field name; update the newest versions[] object in manifest.json to use
the schema-required key ledmatrix_min instead of ledmatrix_min_version and
ensure the object includes released (date) and version (semver) fields in that
order at the top of the array; locate the versions array in manifest.json and
rename the property from "ledmatrix_min_version" to "ledmatrix_min" for the new
release entry.
In `@plugins/masters-tournament/masters_data.py`:
- Around line 223-235: fetch_schedule currently assumes fetch_leaderboard will
populate _last_raw_leaderboard, but when leaderboard responses are served from
cache fetch_leaderboard returns parsed data and leaves _last_raw_leaderboard
None causing warm-cache/off-season breakage; update the code so that
fetch_leaderboard either (A) stores the raw leaderboard payload into
_last_raw_leaderboard and writes that raw payload into the cache via
cache_manager (e.g., cache key for "raw_leaderboard"), or (B) derives tee times
during fetch_leaderboard and caches the parsed tee-times directly; then change
fetch_schedule to read from the raw_leaderboard cache or cached tee-times
instead of relying on _last_raw_leaderboard, and add a gate that verifies cached
metadata (is_masters == True) before parsing tee times using
_parse_tee_times_from_leaderboard to avoid parsing non-Masters payloads.
- Around line 138-141: When event.get("endDate") is missing, the current
fallback end_date = start_date + timedelta(days=3) lands at the start of Sunday;
change the fallback in the block that computes start_date/end_date (referencing
start_date, end_date, and _parse_iso_utc) so that when start_date exists and
endDate is absent you extend through the full final day (e.g., set end_date =
start_date + timedelta(days=3, hours=23, minutes=59, seconds=59) or set the date
to start_date + 3 days then replace the time to 23:59:59.999999) so downstream
phase checks treat all of Sunday as in-tournament rather than post-tournament.
🪄 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: 115b5de5-b678-44de-bded-aedc275e64ab
📒 Files selected for processing (6)
plugins.jsonplugins/masters-tournament/manager.pyplugins/masters-tournament/manifest.jsonplugins/masters-tournament/masters_data.pyplugins/masters-tournament/masters_helpers.pyplugins/masters-tournament/masters_renderer.py
| { | ||
| "version": "2.1.0", | ||
| "released": "2026-04-09", | ||
| "ledmatrix_min_version": "2.0.0" | ||
| }, |
There was a problem hiding this comment.
Use ledmatrix_min in the new release entry.
The new versions[] item is still using ledmatrix_min_version, so it does not match the manifest schema this repo expects for release metadata.
Suggested fix
{
"version": "2.1.0",
"released": "2026-04-09",
- "ledmatrix_min_version": "2.0.0"
+ "ledmatrix_min": "2.0.0"
},As per coding guidelines, "Add the new version FIRST (most recent at top) to the versions array in manifest.json, with fields: released (date), version (semver), and ledmatrix_min (minimum LEDMatrix version)".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| { | |
| "version": "2.1.0", | |
| "released": "2026-04-09", | |
| "ledmatrix_min_version": "2.0.0" | |
| }, | |
| { | |
| "version": "2.1.0", | |
| "released": "2026-04-09", | |
| "ledmatrix_min": "2.0.0" | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@plugins/masters-tournament/manifest.json` around lines 46 - 50, The new entry
in the manifest's versions array uses the wrong field name; update the newest
versions[] object in manifest.json to use the schema-required key ledmatrix_min
instead of ledmatrix_min_version and ensure the object includes released (date)
and version (semver) fields in that order at the top of the array; locate the
versions array in manifest.json and rename the property from
"ledmatrix_min_version" to "ledmatrix_min" for the new release entry.
… bug
Two real bugs surfaced during PR review. Verified both against live ESPN data
before fixing; rejected a third finding about manifest field naming after
confirming the whole repo uses `ledmatrix_min_version`.
1) Warm-cache / off-season tee-time bug
fetch_schedule() relied on self._last_raw_leaderboard, which only got set
when fetch_leaderboard() made an actual HTTP call. Two failing paths:
- Process restart with persistent warm cache: fetch_leaderboard returns
the cached parsed list without populating _last_raw_leaderboard, so
the follow-up schedule parse sees None and returns empty.
- Off-season: _last_raw_leaderboard holds a non-Masters PGA event's
payload (e.g. RBC Heritage), and fetch_schedule would render its tee
times as "Masters pairings".
Fix: derive tee times inside fetch_leaderboard() from the raw response
and cache them directly under CACHE_KEY_SCHEDULE. Tee-time parsing is
gated on meta.is_masters, and when meta is *not* Masters we clear the
schedule cache. fetch_schedule() becomes a pure cache read; on a cold
cache it triggers a leaderboard refresh (which will populate the
schedule cache as a side effect when applicable). The stale
_last_raw_leaderboard instance field is removed entirely.
2) Sunday phase bug (applies to real ESPN data, not just the fallback)
ESPN's events[0].endDate is the *start* of the final calendar day in ET
— for 2026 it returns 2026-04-12T04:00Z = Sun Apr 12 00:00 ET. Any phase
check during Sunday play (say 15:00 ET / 19:00 UTC) would see
now > end_date and return post-tournament while play is still live.
Fix: after parsing endDate (or computing the fallback), add
23h59m59s so the window covers the full final calendar day. Applied
in both _parse_tournament_meta() and _computed_fallback_meta() for
consistency.
Verified every Sunday hour (14:00, 19:00, 22:00, 23:00 UTC) now reports
"tournament" / "tournament-live" against live meta.
3) Rejected: the PR review suggested the versions[] schema requires
`ledmatrix_min` instead of `ledmatrix_min_version`. Repo-wide grep
confirms all 30 other plugin manifests use `ledmatrix_min_version`,
matching our entry. No change made.
Bumps manifest 2.1.0 -> 2.1.1.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
plugins/masters-tournament/masters_data.py (1)
148-158:⚠️ Potential issue | 🟠 MajorThe fallback
end_datestill extends into Monday morning.When
endDateis missing,start_date + timedelta(days=3)keeps Thursday's tee-off hour, and the unconditional+ 23:59:59pushes the fallback past the end of Sunday. Normalize to the final calendar day before setting23:59:59instead of adding another time fragment.Suggested fix
start_date = self._parse_iso_utc(event.get("date")) end_date = self._parse_iso_utc(event.get("endDate")) if end_date is None and start_date is not None: - # Fallback: Masters is always a 4-day (Thu–Sun) event. - end_date = start_date + timedelta(days=3) - if end_date is not None: + # Fallback: Masters is always a 4-day (Thu-Sun) event. + end_date = (start_date + timedelta(days=3)).replace( + hour=23, minute=59, second=59, microsecond=0 + ) + elif end_date is not None: # ESPN reports endDate as the *start* of the final day in ET # (e.g. 2026-04-12T04:00Z = Sun Apr 12 00:00 ET). Push to end # of that calendar day so phase checks treat all of Sunday's # play as in-tournament rather than post-tournament. - end_date = end_date + timedelta(hours=23, minutes=59, seconds=59) + end_date = end_date.replace( + hour=23, minute=59, second=59, microsecond=0 + )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@plugins/masters-tournament/masters_data.py` around lines 148 - 158, The fallback logic for end_date uses start_date + timedelta(days=3) which preserves the tee-off time and then the unconditional + timedelta(hours=23, minutes=59, seconds=59) pushes the end into Monday; change the fallback to normalize to the final calendar day first (e.g. take start_date.date() + 3 days and convert to a datetime at 00:00 UTC) and then add the 23:59:59 adjustment so end_date represents the end of Sunday; update the code around _parse_iso_utc, start_date, end_date and the timedelta arithmetic accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@plugins/masters-tournament/masters_data.py`:
- Around line 424-455: The tee time entries built in masters_data.py currently
set the "time" key to raw ISO timestamps (from explicit startTime or
status.teeTime); before returning/caching, convert those ISO strings to a
human-friendly display string (e.g., local or formatted "HH:MM" with timezone)
and store that under a display key such as "time_display" while preserving the
raw ISO in "time" (or vice versa), and ensure sorting uses the raw ISO field
(e.g., sort by "time" or "time_raw") so masters_renderer.py can render the
readable value without showing full ISO strings; update both the explicit branch
(tt.get("startTime")) and the fallback branch (status.get("teeTime")) when
constructing result entries.
- Around line 69-82: The code caches tournament metadata into CACHE_KEY_META
unconditionally after calling _parse_tournament_meta, which allows non-Masters
events to override the Masters window used by fetch_tournament_meta(); change
the logic so you only write meta to cache_manager.set(CACHE_KEY_META, ...) if
meta is truthy AND meta.get("is_masters") is true (i.e., move or guard the
CACHE_KEY_META set behind the is_masters check), so non-Masters metadata is
never stored under CACHE_KEY_META and the plugin continues to use correct
Masters countdown/phase/TTL.
---
Duplicate comments:
In `@plugins/masters-tournament/masters_data.py`:
- Around line 148-158: The fallback logic for end_date uses start_date +
timedelta(days=3) which preserves the tee-off time and then the unconditional +
timedelta(hours=23, minutes=59, seconds=59) pushes the end into Monday; change
the fallback to normalize to the final calendar day first (e.g. take
start_date.date() + 3 days and convert to a datetime at 00:00 UTC) and then add
the 23:59:59 adjustment so end_date represents the end of Sunday; update the
code around _parse_iso_utc, start_date, end_date and the timedelta arithmetic
accordingly.
🪄 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: 75af96ac-6389-4029-98e6-acdaeb6be45d
📒 Files selected for processing (3)
plugins.jsonplugins/masters-tournament/manifest.jsonplugins/masters-tournament/masters_data.py
✅ Files skipped from review due to trivial changes (1)
- plugins.json
🚧 Files skipped from review as they are similar to previous changes (1)
- plugins/masters-tournament/manifest.json
…on meta cache
Second round of PR review follow-ups. Verified each finding against current
code; applied two fixes and rejected one.
1) Tee-time display formatting (renderer was drawing raw ISO)
masters_renderer.render_schedule reads entry["time"] and draws it
verbatim. Before this change, that value was the raw ESPN ISO string
(e.g. '2026-04-09T11:40Z'), which is 17 characters of UTC nonsense
on an LED matrix.
Fix: _parse_tee_times_from_leaderboard now populates "time" with a
human-readable Augusta-local string ('7:40 AM') and keeps the raw
ISO under "time_raw". Sorting uses "time_raw" so pairings stay in
chronological order regardless of the display string. Applied to both
the explicit teeTimes branch and the fallback group-by-status.teeTime
branch. New helper MastersDataSource._format_tee_time_et() handles
parsing, ET conversion (Masters is always played in the second week
of April, well after US DST starts, so EDT/UTC-4 is always correct),
and 12-hour formatting. Unparseable values render as "TBD".
2) CACHE_KEY_META poisoning during off-season
_parse_tournament_meta was writing whatever ESPN returned — including
non-Masters events like RBC Heritage — into CACHE_KEY_META, which
fetch_tournament_meta would then serve to _display_countdown, pointing
the countdown at the wrong tournament.
Fix: gate the cache_manager.set(CACHE_KEY_META, ...) call on
meta.get("is_masters"). With the gate, off-season runs fall through
to _computed_fallback_meta() (second-Thursday-of-April), which is the
correct behavior. Verified by feeding a fake RBC Heritage payload
through fetch_leaderboard and confirming CACHE_KEY_META stays empty.
3) Rejected: fallback end_date math
Review suggested normalizing start_date.date() + 3 days to 00:00 UTC
before adding 23:59:59. Traced the actual datetime math: ESPN's date
field is already 04:00 UTC (= midnight ET), so current code
(start + 3d + 23:59:59) lands at 2026-04-13 03:59:59 UTC = 23:59:59
EDT Sun Apr 12 — exactly end-of-Sunday in Augusta local time. The
proposed fix would truncate to 19:59:59 EDT (8pm Sunday), cutting
off trophy ceremony and any weather-delayed finish. No change.
Validated against live ESPN data:
- 31 pairings render as '7:40 AM', '7:50 AM', ..., '10:50 AM' — sorted
chronologically by underlying ISO, not by display string
- Off-season fake RBC Heritage payload: CACHE_KEY_META stays empty,
fetch_tournament_meta falls through to computed fallback (is_masters=False)
- Control case (real Masters payload): CACHE_KEY_META populated,
is_masters=True, name='Masters Tournament'
- test_plugin_standalone.py: 45 passing checks, no failures
Bumps manifest 2.1.1 -> 2.1.2.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
plugins/masters-tournament/masters_data.py (1)
420-429:⚠️ Potential issue | 🟡 MinorRemove the unused
_is_masters_tournamentmethod.This method is never called anywhere in the codebase and duplicates the Masters-detection logic from
_parse_tournament_meta(lines 147-149). Removing it will eliminate maintenance burden and improve code clarity.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@plugins/masters-tournament/masters_data.py` around lines 420 - 429, Delete the unused helper method _is_masters_tournament from masters_data.py: remove the entire def _is_masters_tournament(self, data: Dict) -> bool block and any associated imports/variables it solely supports; ensure there are no remaining references to _is_masters_tournament elsewhere, and rely on the existing Masters-detection logic in _parse_tournament_meta to cover that functionality.
🧹 Nitpick comments (2)
plugins/masters-tournament/masters_data.py (2)
617-619: Consider logging exceptions instead of silently passing.The bare
except Exception: passswallows all errors without any trace. Adding a debug-level log would aid troubleshooting when ESPN data doesn't parse as expected.Suggested fix
- except Exception: - pass + except Exception as e: + self.logger.debug(f"Error extracting round scores: {e}")🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@plugins/masters-tournament/masters_data.py` around lines 617 - 619, Replace the bare "except Exception: pass" that returns "rounds" with an exception handler that logs the error (e.g., "except Exception as e: logger.debug('Error parsing ESPN rounds', exc_info=True)") so failures are recorded for debugging; locate the try/except surrounding the "rounds" return and use the module's existing logger instance (or import logging and getLogger) to emit a debug-level log including exc_info=True and the offending exception variable.
382-384: Consider usingstrict=Trueinzip()to catch length mismatches.If ESPN's API shape changes such that
labelsandvalueshave different lengths, the current code silently truncates data. Addingstrict=True(supported in Python 3.11) will raise an error on length mismatches, surfacing API contract violations early rather than silently losing data.Suggested fix
- for label, value in zip(labels, values): + for label, value in zip(labels, values, strict=True):🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@plugins/masters-tournament/masters_data.py` around lines 382 - 384, The loop using zip(labels, values) in the stats assembly silently truncates when lengths differ; change the call to zip(labels, values, strict=True) in that loop (the for label, value in zip(...) block) so Python raises on mismatches, and add a short try/except around that loop to catch ValueError and log or re-raise a clear error (include the labels/values lengths and context) so API shape changes are surfaced early.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@plugins/masters-tournament/masters_data.py`:
- Line 37: The HTTP_HEADERS constant declares the User-Agent as "LEDMatrix
Masters Plugin/2.1" which is out of sync with the manifest bumped to 2.1.2;
update the HTTP_HEADERS value (symbol: HTTP_HEADERS) to use "LEDMatrix Masters
Plugin/2.1.2" so server logs reflect the actual plugin version (or alternatively
load the version from the package/manifest and construct the User-Agent string
dynamically).
---
Outside diff comments:
In `@plugins/masters-tournament/masters_data.py`:
- Around line 420-429: Delete the unused helper method _is_masters_tournament
from masters_data.py: remove the entire def _is_masters_tournament(self, data:
Dict) -> bool block and any associated imports/variables it solely supports;
ensure there are no remaining references to _is_masters_tournament elsewhere,
and rely on the existing Masters-detection logic in _parse_tournament_meta to
cover that functionality.
---
Nitpick comments:
In `@plugins/masters-tournament/masters_data.py`:
- Around line 617-619: Replace the bare "except Exception: pass" that returns
"rounds" with an exception handler that logs the error (e.g., "except Exception
as e: logger.debug('Error parsing ESPN rounds', exc_info=True)") so failures are
recorded for debugging; locate the try/except surrounding the "rounds" return
and use the module's existing logger instance (or import logging and getLogger)
to emit a debug-level log including exc_info=True and the offending exception
variable.
- Around line 382-384: The loop using zip(labels, values) in the stats assembly
silently truncates when lengths differ; change the call to zip(labels, values,
strict=True) in that loop (the for label, value in zip(...) block) so Python
raises on mismatches, and add a short try/except around that loop to catch
ValueError and log or re-raise a clear error (include the labels/values lengths
and context) so API shape changes are surfaced early.
🪄 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: dd7fd232-5b11-4cf6-8b52-16111b750d4b
📒 Files selected for processing (3)
plugins.jsonplugins/masters-tournament/manifest.jsonplugins/masters-tournament/masters_data.py
✅ Files skipped from review due to trivial changes (1)
- plugins.json
🚧 Files skipped from review as they are similar to previous changes (1)
- plugins/masters-tournament/manifest.json
| ATHLETE_URL = "https://site.web.api.espn.com/apis/common/v3/sports/golf/pga/athletes/{player_id}" | ||
| ATHLETE_OVERVIEW_URL = "https://site.web.api.espn.com/apis/common/v3/sports/golf/pga/athletes/{player_id}/overview" | ||
|
|
||
| HTTP_HEADERS = {"User-Agent": "LEDMatrix Masters Plugin/2.1"} |
There was a problem hiding this comment.
User-Agent version is out of sync with manifest.
The HTTP header declares Plugin/2.1 but the manifest was bumped to 2.1.2 per the PR objectives. Keep these synchronized so server-side logs reflect the actual plugin version.
Suggested fix
- HTTP_HEADERS = {"User-Agent": "LEDMatrix Masters Plugin/2.1"}
+ HTTP_HEADERS = {"User-Agent": "LEDMatrix Masters Plugin/2.1.2"}As per coding guidelines: **/plugins/**/*.py: Bump PATCH version (1.2.x) for bug fixes, performance improvements, documentation updates, or minor tweaks.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| HTTP_HEADERS = {"User-Agent": "LEDMatrix Masters Plugin/2.1"} | |
| HTTP_HEADERS = {"User-Agent": "LEDMatrix Masters Plugin/2.1.2"} |
🧰 Tools
🪛 Ruff (0.15.9)
[warning] 37-37: Mutable default value for class attribute
(RUF012)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@plugins/masters-tournament/masters_data.py` at line 37, The HTTP_HEADERS
constant declares the User-Agent as "LEDMatrix Masters Plugin/2.1" which is out
of sync with the manifest bumped to 2.1.2; update the HTTP_HEADERS value
(symbol: HTTP_HEADERS) to use "LEDMatrix Masters Plugin/2.1.2" so server logs
reflect the actual plugin version (or alternatively load the version from the
package/manifest and construct the User-Agent string dynamically).
…cache guard
Addresses three user reports from Discord.
## Report 3 (JScottyR): Vegas scroll cards spanning the full panel
On a 64×64 × 5-panel chain (320×64), masters was rendering each player
card at the full panel size, so "Vegas scroll" mode showed one giant card
at a time instead of a ticker of smaller fixed-size blocks. Every other
sports scoreboard in the repo follows a fixed-block convention — football,
hockey, and baseball all default `game_card_width=128` regardless of the
physical display width, with cards constructed at that explicit size.
Fix:
- New top-level config key `scroll_card_width` (default 128, min 32,
max 256) in config_schema.json with a description referencing the
Vegas scroll ticker mode.
- `manager.py` reads it as `self._scroll_card_width` in `__init__` (and
on hot-reload), then passes `card_width=self._scroll_card_width,
card_height=self.display_height` to each `render_*` call inside
`get_vegas_content()`.
- `render_player_card()`, `render_hole_card()`, and `render_fun_fact()`
in `masters_renderer.py` now accept optional `card_width`/`card_height`
parameters. When provided, the body uses local `w`/`h`/`cw`/`ch`
variables instead of `self.width`/`self.height`, and `is_wide_short`
is recomputed per-card (so a 128×64 block on a 320×64 panel is
aspect 2.0 and uses the standard vertical-stack layout, while the
same panel's full-screen modes still use the two-column wide-short
layout for the leaderboard, schedule, etc).
- `_render_player_card_wide_short()` takes the same `w`/`h` overrides
and parameterizes every `self.width`/`self.height` reference.
- `_draw_gradient_bg()` accepts optional `width`/`height` so each render
can allocate its own image at the override size.
- Enhanced renderer overrides (`render_player_card`, `render_hole_card`)
delegate to `super()` when called with explicit card dimensions, since
the enhanced round-scores block and left-panel-plus-image layouts are
designed for full-panel rendering and don't fit at block sizes.
Verified locally at every `(parent, card)` combination in
`{(320,64), (384,64), (192,48), (128,64), (64,32)} × {(128,64), (128,48),
(80,64), (64,32)}`: every returned image's `.size` exactly matches the
override. A simulated `get_vegas_content()` on a 320×64 parent with
default `scroll_card_width=128` produces 33 cards (10 players, 18 holes,
5 facts), ALL at exactly 128×64. A 5-card scroll strip concatenated
side-by-side renders as a 640×64 image that looks like the football
scoreboard ticker pattern — headshot + name + country + score + pos/thru
per card, cleanly repeated.
## Reports 1 & 2 (Fish Man): defensive stale-cache guard
Reports 1 (`'str' object has no attribute 'tzinfo'`) and 2 (`'<=' not
supported between instances of 'float' and 'NoneType'`) are both already
fixed in PR #95 via `_rehydrate_meta()` and `_NEVER_EXPIRE` respectively,
but neither has merged to `main` yet — a user who pulled the plugin-store
update to 2.1.2 after PR #94 merged is running the exact version where
both errors fire.
Residual risk even after PR #95 / this branch ships: the disk cache file
`/var/cache/ledmatrix/masters_tournament_meta.json` persists from the
broken 2.1.2 run. The core `CacheManager` logs the `<= None` error and
returns None (cache miss) rather than re-raising, but the log noise
persists every tick until the file is overwritten by a successful fetch.
Added `MastersDataSource._safe_cache_get()` that wraps every
`cache_manager.get()` call in a try/except, treats any exception as a
cache miss, and logs a single warning per instance (not per tick)
pointing at the stale file path. Replaced all 9 `cache_manager.get()`
call sites in `masters_data.py` with the safe wrapper.
Verified with a stub `BrokenCache` that raises `TypeError('<=' not
supported...)` on every `.get()` AND a blocked network: the plugin
still initializes without crashing, returns the computed second-Thursday-
of-April fallback meta, falls back to mock leaderboard data, and logs
the stale-cache warning exactly once.
## Other
- Bumps manifest `2.2.4 → 2.2.5`.
- test_plugin_standalone.py: 45/45 still passing.
Depends on PR #95 being merged first (same branch base).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…cache guard
Addresses three user reports from Discord.
## Report 3 (JScottyR): Vegas scroll cards spanning the full panel
On a 64×64 × 5-panel chain (320×64), masters was rendering each player
card at the full panel size, so "Vegas scroll" mode showed one giant card
at a time instead of a ticker of smaller fixed-size blocks. Every other
sports scoreboard in the repo follows a fixed-block convention — football,
hockey, and baseball all default `game_card_width=128` regardless of the
physical display width, with cards constructed at that explicit size.
Fix:
- New top-level config key `scroll_card_width` (default 128, min 32,
max 256) in config_schema.json with a description referencing the
Vegas scroll ticker mode.
- `manager.py` reads it as `self._scroll_card_width` in `__init__` (and
on hot-reload), then passes `card_width=self._scroll_card_width,
card_height=self.display_height` to each `render_*` call inside
`get_vegas_content()`.
- `render_player_card()`, `render_hole_card()`, and `render_fun_fact()`
in `masters_renderer.py` now accept optional `card_width`/`card_height`
parameters. When provided, the body uses local `w`/`h`/`cw`/`ch`
variables instead of `self.width`/`self.height`, and `is_wide_short`
is recomputed per-card (so a 128×64 block on a 320×64 panel is
aspect 2.0 and uses the standard vertical-stack layout, while the
same panel's full-screen modes still use the two-column wide-short
layout for the leaderboard, schedule, etc).
- `_render_player_card_wide_short()` takes the same `w`/`h` overrides
and parameterizes every `self.width`/`self.height` reference.
- `_draw_gradient_bg()` accepts optional `width`/`height` so each render
can allocate its own image at the override size.
- Enhanced renderer overrides (`render_player_card`, `render_hole_card`)
delegate to `super()` when called with explicit card dimensions, since
the enhanced round-scores block and left-panel-plus-image layouts are
designed for full-panel rendering and don't fit at block sizes.
Verified locally at every `(parent, card)` combination in
`{(320,64), (384,64), (192,48), (128,64), (64,32)} × {(128,64), (128,48),
(80,64), (64,32)}`: every returned image's `.size` exactly matches the
override. A simulated `get_vegas_content()` on a 320×64 parent with
default `scroll_card_width=128` produces 33 cards (10 players, 18 holes,
5 facts), ALL at exactly 128×64. A 5-card scroll strip concatenated
side-by-side renders as a 640×64 image that looks like the football
scoreboard ticker pattern — headshot + name + country + score + pos/thru
per card, cleanly repeated.
## Reports 1 & 2 (Fish Man): defensive stale-cache guard
Reports 1 (`'str' object has no attribute 'tzinfo'`) and 2 (`'<=' not
supported between instances of 'float' and 'NoneType'`) are both already
fixed in PR #95 via `_rehydrate_meta()` and `_NEVER_EXPIRE` respectively,
but neither has merged to `main` yet — a user who pulled the plugin-store
update to 2.1.2 after PR #94 merged is running the exact version where
both errors fire.
Residual risk even after PR #95 / this branch ships: the disk cache file
`/var/cache/ledmatrix/masters_tournament_meta.json` persists from the
broken 2.1.2 run. The core `CacheManager` logs the `<= None` error and
returns None (cache miss) rather than re-raising, but the log noise
persists every tick until the file is overwritten by a successful fetch.
Added `MastersDataSource._safe_cache_get()` that wraps every
`cache_manager.get()` call in a try/except, treats any exception as a
cache miss, and logs a single warning per instance (not per tick)
pointing at the stale file path. Replaced all 9 `cache_manager.get()`
call sites in `masters_data.py` with the safe wrapper.
Verified with a stub `BrokenCache` that raises `TypeError('<=' not
supported...)` on every `.get()` AND a blocked network: the plugin
still initializes without crashing, returns the computed second-Thursday-
of-April fallback meta, falls back to mock leaderboard data, and logs
the stale-cache warning exactly once.
## Other
- Bumps manifest `2.2.4 → 2.2.5`.
- test_plugin_standalone.py: 45/45 still passing.
Depends on PR #95 being merged first (same branch base).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…d (2.2.5) (#96) * fix(masters-tournament): Vegas scroll block sizing + defensive stale-cache guard Addresses three user reports from Discord. ## Report 3 (JScottyR): Vegas scroll cards spanning the full panel On a 64×64 × 5-panel chain (320×64), masters was rendering each player card at the full panel size, so "Vegas scroll" mode showed one giant card at a time instead of a ticker of smaller fixed-size blocks. Every other sports scoreboard in the repo follows a fixed-block convention — football, hockey, and baseball all default `game_card_width=128` regardless of the physical display width, with cards constructed at that explicit size. Fix: - New top-level config key `scroll_card_width` (default 128, min 32, max 256) in config_schema.json with a description referencing the Vegas scroll ticker mode. - `manager.py` reads it as `self._scroll_card_width` in `__init__` (and on hot-reload), then passes `card_width=self._scroll_card_width, card_height=self.display_height` to each `render_*` call inside `get_vegas_content()`. - `render_player_card()`, `render_hole_card()`, and `render_fun_fact()` in `masters_renderer.py` now accept optional `card_width`/`card_height` parameters. When provided, the body uses local `w`/`h`/`cw`/`ch` variables instead of `self.width`/`self.height`, and `is_wide_short` is recomputed per-card (so a 128×64 block on a 320×64 panel is aspect 2.0 and uses the standard vertical-stack layout, while the same panel's full-screen modes still use the two-column wide-short layout for the leaderboard, schedule, etc). - `_render_player_card_wide_short()` takes the same `w`/`h` overrides and parameterizes every `self.width`/`self.height` reference. - `_draw_gradient_bg()` accepts optional `width`/`height` so each render can allocate its own image at the override size. - Enhanced renderer overrides (`render_player_card`, `render_hole_card`) delegate to `super()` when called with explicit card dimensions, since the enhanced round-scores block and left-panel-plus-image layouts are designed for full-panel rendering and don't fit at block sizes. Verified locally at every `(parent, card)` combination in `{(320,64), (384,64), (192,48), (128,64), (64,32)} × {(128,64), (128,48), (80,64), (64,32)}`: every returned image's `.size` exactly matches the override. A simulated `get_vegas_content()` on a 320×64 parent with default `scroll_card_width=128` produces 33 cards (10 players, 18 holes, 5 facts), ALL at exactly 128×64. A 5-card scroll strip concatenated side-by-side renders as a 640×64 image that looks like the football scoreboard ticker pattern — headshot + name + country + score + pos/thru per card, cleanly repeated. ## Reports 1 & 2 (Fish Man): defensive stale-cache guard Reports 1 (`'str' object has no attribute 'tzinfo'`) and 2 (`'<=' not supported between instances of 'float' and 'NoneType'`) are both already fixed in PR #95 via `_rehydrate_meta()` and `_NEVER_EXPIRE` respectively, but neither has merged to `main` yet — a user who pulled the plugin-store update to 2.1.2 after PR #94 merged is running the exact version where both errors fire. Residual risk even after PR #95 / this branch ships: the disk cache file `/var/cache/ledmatrix/masters_tournament_meta.json` persists from the broken 2.1.2 run. The core `CacheManager` logs the `<= None` error and returns None (cache miss) rather than re-raising, but the log noise persists every tick until the file is overwritten by a successful fetch. Added `MastersDataSource._safe_cache_get()` that wraps every `cache_manager.get()` call in a try/except, treats any exception as a cache miss, and logs a single warning per instance (not per tick) pointing at the stale file path. Replaced all 9 `cache_manager.get()` call sites in `masters_data.py` with the safe wrapper. Verified with a stub `BrokenCache` that raises `TypeError('<=' not supported...)` on every `.get()` AND a blocked network: the plugin still initializes without crashing, returns the computed second-Thursday- of-April fallback meta, falls back to mock leaderboard data, and logs the stale-cache warning exactly once. ## Other - Bumps manifest `2.2.4 → 2.2.5`. - test_plugin_standalone.py: 45/45 still passing. Depends on PR #95 being merged first (same branch base). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(masters-tournament): hole card layout per effective dims + Masters wordmark assets ## Hole card layout (bundled with Vegas scroll fix) The earlier Vegas scroll fix (2.2.5) had the enhanced `render_hole_card` delegating to the base class when called with `card_width`/`card_height` overrides. The base class uses a different layout (horizontal header + centered image + footer) that's neither the user's desired "1 text column + image" for larger cards nor the "2 text columns, no image" for smaller ones — so Vegas scroll hole cards looked inconsistent with the full-panel layouts. Rewrote the enhanced `render_hole_card` to: 1. Accept `card_width`/`card_height` directly and render at those dimensions. 2. Choose between two layouts based on the EFFECTIVE card dimensions (not `self.tier`), using thresholds `cw >= 96 AND ch >= 40`: * **Large enough for image** → single left-column text [Hole #, Name, Par, Yards] + hole image on the right, zone badge in the bottom-right. Split into a new helper `_render_hole_card_with_image()`. * **Smaller canvases** → two text columns: [# / Name] | [Par / Yards / Zone], no image. `_render_hole_card_compact()` now also accepts `cw`/`ch` overrides. 3. `left_w` (text column width) now scales with `cw` via `max(38, min(56, cw // 3))` so 256x64 cards get a roomier text column than 128x48 blocks. Verified across the full size matrix: * 192x48 full panel → IMAGE (#12 + Golden Bell + Par 3 + 155y + map + AMEN CORNER) * 128x64 full panel → IMAGE * 256x64 full panel → IMAGE * 320x64 Vegas 128x64 → IMAGE (inside a scrollable block, not full-panel) * 320x64 Vegas 128x48 → IMAGE * 192x48 Vegas 128x48 → IMAGE * 320x64 Vegas 80x64 → COMPACT 2-col (too narrow for image) * 128x32 full panel → COMPACT 2-col * 128x32 Vegas 80x32 → COMPACT 2-col * 64x32 full panel → COMPACT 2-col Both layouts have the user's requested content layout: * Image layout: one text column [Hole #, Hole Name, Par, Yards] * Compact layout: col 1 [Hole # + Name], col 2 [Par, Yards, Zone] ## Masters wordmark assets Extracted from `masters-icons.ttf` (IcoMoon icon font from masters.com). The font has 143 glyphs in the U+E900–U+E98F Private Use Area, mostly UI icons, but scanning for unusually-wide advance widths revealed two wordmarks: * U+E954 (5.95 em) - the iconic MASTERS wordmark with the Augusta National contour + fairway flag + "Masters" in italic serif * U+E95B (4.89 em) - AUGUSTA wordmark in matching italic serif Added to `plugins/masters-tournament/assets/masters/logos/`: * wordmark_32.png 191x32 white on transparent * wordmark_48.png 286x49 * wordmark_64.png 381x64 * wordmark_128.png 762x128 * augusta_wordmark_32.png 156x30 * augusta_wordmark_48.png 235x45 Assets only — not yet wired into the renderer. A future change can swap these in for the current `masters_logo_*.png` in countdown, player card headers, etc. Bumps manifest 2.2.5 → 2.2.6. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(masters-tournament): pixel-perfect 5x7 BDF font for compact hole card Previous attempts to use 5by7.regular.ttf via _load_font_sized() resulted in washed-out anti-aliased text because PIL's TTF renderer smooths small pixel fonts. The 5x7.bdf asset in the LEDMatrix core assets dir is a true fixed-size bitmap font, and PIL.BdfFontFile can convert it to PIL format for pixel-perfect 1:1 rendering. Changes: - Added _load_bdf_font(filename) in masters_renderer.py. Converts BDF → PIL format once per process via tempfile.mkdtemp(), caches the loaded ImageFont in _BDF_FONT_CACHE (keyed by filename), and stores failure results so missing files don't re-hit the disk. Uses a local `from PIL import BdfFontFile` import since it's only needed by this helper. - Enhanced renderer imports _load_bdf_font alongside _load_font_sized. - _render_hole_card_compact() now loads 5x7.bdf for both text_font and hole_font. The BDF renders at its native 7px height with no anti-aliasing, so glyphs look crisp and bold. Falls back to self.font_detail / self.font_body when the BDF file isn't on the search path. - The previous 4x6 code is kept in a commented block directly underneath so we can flip fonts back in one edit if users report issues with 5x7 on actual hardware. Verified: - _load_bdf_font("5x7.bdf") returns a valid ImageFont, 'Ag' bbox height exactly 7px, second call hits the cache (same object id). - _load_bdf_font("nonexistent.bdf") returns None cleanly. - Render matrix across full panels (64x32, 128x32, 128x48, 128x64, 192x48, 256x64) and Vegas scroll blocks (128x64, 128x48, 128x32, 80x32) all produce expected layouts: * ch >= 48 → IMAGE layout (left-panel text + hole image, unchanged) * ch < 48 → COMPACT 2-col (# + Name left, Par + Yards right) - Compact layouts now render with pixel-perfect 5x7 glyphs; the narrower-per-glyph 5x7 font also lets "AMEN CORNER" fit fully on a 128-wide compact card where 4x6 was truncating to "AMEN COR". - test_plugin_standalone.py: 45/45 passing. Note: 5x7.bdf is NOT copied into the plugin repo — it already lives in the core LEDMatrix assets dir which the plugin's FONT_SEARCH_DIRS already knows about. If a future deployment doesn't ship the core fonts, the fallback to self.font_detail keeps the hole card working. Bumps manifest 2.2.6 → 2.2.7. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): Vegas scroll block sizing + defensive stale-cache guard
Addresses three user reports from Discord.
## Report 3 (JScottyR): Vegas scroll cards spanning the full panel
On a 64×64 × 5-panel chain (320×64), masters was rendering each player
card at the full panel size, so "Vegas scroll" mode showed one giant card
at a time instead of a ticker of smaller fixed-size blocks. Every other
sports scoreboard in the repo follows a fixed-block convention — football,
hockey, and baseball all default `game_card_width=128` regardless of the
physical display width, with cards constructed at that explicit size.
Fix:
- New top-level config key `scroll_card_width` (default 128, min 32,
max 256) in config_schema.json with a description referencing the
Vegas scroll ticker mode.
- `manager.py` reads it as `self._scroll_card_width` in `__init__` (and
on hot-reload), then passes `card_width=self._scroll_card_width,
card_height=self.display_height` to each `render_*` call inside
`get_vegas_content()`.
- `render_player_card()`, `render_hole_card()`, and `render_fun_fact()`
in `masters_renderer.py` now accept optional `card_width`/`card_height`
parameters. When provided, the body uses local `w`/`h`/`cw`/`ch`
variables instead of `self.width`/`self.height`, and `is_wide_short`
is recomputed per-card (so a 128×64 block on a 320×64 panel is
aspect 2.0 and uses the standard vertical-stack layout, while the
same panel's full-screen modes still use the two-column wide-short
layout for the leaderboard, schedule, etc).
- `_render_player_card_wide_short()` takes the same `w`/`h` overrides
and parameterizes every `self.width`/`self.height` reference.
- `_draw_gradient_bg()` accepts optional `width`/`height` so each render
can allocate its own image at the override size.
- Enhanced renderer overrides (`render_player_card`, `render_hole_card`)
delegate to `super()` when called with explicit card dimensions, since
the enhanced round-scores block and left-panel-plus-image layouts are
designed for full-panel rendering and don't fit at block sizes.
Verified locally at every `(parent, card)` combination in
`{(320,64), (384,64), (192,48), (128,64), (64,32)} × {(128,64), (128,48),
(80,64), (64,32)}`: every returned image's `.size` exactly matches the
override. A simulated `get_vegas_content()` on a 320×64 parent with
default `scroll_card_width=128` produces 33 cards (10 players, 18 holes,
5 facts), ALL at exactly 128×64. A 5-card scroll strip concatenated
side-by-side renders as a 640×64 image that looks like the football
scoreboard ticker pattern — headshot + name + country + score + pos/thru
per card, cleanly repeated.
## Reports 1 & 2 (Fish Man): defensive stale-cache guard
Reports 1 (`'str' object has no attribute 'tzinfo'`) and 2 (`'<=' not
supported between instances of 'float' and 'NoneType'`) are both already
fixed in PR #95 via `_rehydrate_meta()` and `_NEVER_EXPIRE` respectively,
but neither has merged to `main` yet — a user who pulled the plugin-store
update to 2.1.2 after PR #94 merged is running the exact version where
both errors fire.
Residual risk even after PR #95 / this branch ships: the disk cache file
`/var/cache/ledmatrix/masters_tournament_meta.json` persists from the
broken 2.1.2 run. The core `CacheManager` logs the `<= None` error and
returns None (cache miss) rather than re-raising, but the log noise
persists every tick until the file is overwritten by a successful fetch.
Added `MastersDataSource._safe_cache_get()` that wraps every
`cache_manager.get()` call in a try/except, treats any exception as a
cache miss, and logs a single warning per instance (not per tick)
pointing at the stale file path. Replaced all 9 `cache_manager.get()`
call sites in `masters_data.py` with the safe wrapper.
Verified with a stub `BrokenCache` that raises `TypeError('<=' not
supported...)` on every `.get()` AND a blocked network: the plugin
still initializes without crashing, returns the computed second-Thursday-
of-April fallback meta, falls back to mock leaderboard data, and logs
the stale-cache warning exactly once.
## Other
- Bumps manifest `2.2.4 → 2.2.5`.
- test_plugin_standalone.py: 45/45 still passing.
Depends on PR #95 being merged first (same branch base).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): hole card layout per effective dims + Masters wordmark assets
## Hole card layout (bundled with Vegas scroll fix)
The earlier Vegas scroll fix (2.2.5) had the enhanced `render_hole_card`
delegating to the base class when called with `card_width`/`card_height`
overrides. The base class uses a different layout (horizontal header +
centered image + footer) that's neither the user's desired "1 text column
+ image" for larger cards nor the "2 text columns, no image" for smaller
ones — so Vegas scroll hole cards looked inconsistent with the full-panel
layouts.
Rewrote the enhanced `render_hole_card` to:
1. Accept `card_width`/`card_height` directly and render at those
dimensions.
2. Choose between two layouts based on the EFFECTIVE card dimensions
(not `self.tier`), using thresholds `cw >= 96 AND ch >= 40`:
* **Large enough for image** → single left-column text
[Hole #, Name, Par, Yards] + hole image on the right, zone
badge in the bottom-right. Split into a new helper
`_render_hole_card_with_image()`.
* **Smaller canvases** → two text columns: [# / Name] | [Par /
Yards / Zone], no image. `_render_hole_card_compact()` now
also accepts `cw`/`ch` overrides.
3. `left_w` (text column width) now scales with `cw` via
`max(38, min(56, cw // 3))` so 256x64 cards get a roomier text
column than 128x48 blocks.
Verified across the full size matrix:
* 192x48 full panel → IMAGE (#12 + Golden Bell + Par 3 + 155y + map + AMEN CORNER)
* 128x64 full panel → IMAGE
* 256x64 full panel → IMAGE
* 320x64 Vegas 128x64 → IMAGE (inside a scrollable block, not full-panel)
* 320x64 Vegas 128x48 → IMAGE
* 192x48 Vegas 128x48 → IMAGE
* 320x64 Vegas 80x64 → COMPACT 2-col (too narrow for image)
* 128x32 full panel → COMPACT 2-col
* 128x32 Vegas 80x32 → COMPACT 2-col
* 64x32 full panel → COMPACT 2-col
Both layouts have the user's requested content layout:
* Image layout: one text column [Hole #, Hole Name, Par, Yards]
* Compact layout: col 1 [Hole # + Name], col 2 [Par, Yards, Zone]
## Masters wordmark assets
Extracted from `masters-icons.ttf` (IcoMoon icon font from masters.com).
The font has 143 glyphs in the U+E900–U+E98F Private Use Area, mostly
UI icons, but scanning for unusually-wide advance widths revealed two
wordmarks:
* U+E954 (5.95 em) - the iconic MASTERS wordmark with the Augusta
National contour + fairway flag + "Masters" in italic serif
* U+E95B (4.89 em) - AUGUSTA wordmark in matching italic serif
Added to `plugins/masters-tournament/assets/masters/logos/`:
* wordmark_32.png 191x32 white on transparent
* wordmark_48.png 286x49
* wordmark_64.png 381x64
* wordmark_128.png 762x128
* augusta_wordmark_32.png 156x30
* augusta_wordmark_48.png 235x45
Assets only — not yet wired into the renderer. A future change can
swap these in for the current `masters_logo_*.png` in countdown,
player card headers, etc.
Bumps manifest 2.2.5 → 2.2.6.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): pixel-perfect 5x7 BDF font for compact hole card
Previous attempts to use 5by7.regular.ttf via _load_font_sized() resulted
in washed-out anti-aliased text because PIL's TTF renderer smooths small
pixel fonts. The 5x7.bdf asset in the LEDMatrix core assets dir is a
true fixed-size bitmap font, and PIL.BdfFontFile can convert it to PIL
format for pixel-perfect 1:1 rendering.
Changes:
- Added _load_bdf_font(filename) in masters_renderer.py. Converts
BDF → PIL format once per process via tempfile.mkdtemp(), caches
the loaded ImageFont in _BDF_FONT_CACHE (keyed by filename), and
stores failure results so missing files don't re-hit the disk.
Uses a local `from PIL import BdfFontFile` import since it's only
needed by this helper.
- Enhanced renderer imports _load_bdf_font alongside _load_font_sized.
- _render_hole_card_compact() now loads 5x7.bdf for both text_font
and hole_font. The BDF renders at its native 7px height with no
anti-aliasing, so glyphs look crisp and bold. Falls back to
self.font_detail / self.font_body when the BDF file isn't on the
search path.
- The previous 4x6 code is kept in a commented block directly
underneath so we can flip fonts back in one edit if users report
issues with 5x7 on actual hardware.
Verified:
- _load_bdf_font("5x7.bdf") returns a valid ImageFont, 'Ag' bbox
height exactly 7px, second call hits the cache (same object id).
- _load_bdf_font("nonexistent.bdf") returns None cleanly.
- Render matrix across full panels (64x32, 128x32, 128x48, 128x64,
192x48, 256x64) and Vegas scroll blocks (128x64, 128x48, 128x32,
80x32) all produce expected layouts:
* ch >= 48 → IMAGE layout (left-panel text + hole image, unchanged)
* ch < 48 → COMPACT 2-col (# + Name left, Par + Yards right)
- Compact layouts now render with pixel-perfect 5x7 glyphs; the
narrower-per-glyph 5x7 font also lets "AMEN CORNER" fit fully on
a 128-wide compact card where 4x6 was truncating to "AMEN COR".
- test_plugin_standalone.py: 45/45 passing.
Note: 5x7.bdf is NOT copied into the plugin repo — it already lives
in the core LEDMatrix assets dir which the plugin's FONT_SEARCH_DIRS
already knows about. If a future deployment doesn't ship the core
fonts, the fallback to self.font_detail keeps the hole card working.
Bumps manifest 2.2.6 → 2.2.7.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): layout fixes for 32px-height displays (128x32, 256x32)
Addresses multiple layout issues reported on 2-chain and 4-chain 64x32
displays where the large tier's vertical measurements overflowed 32px.
- Add compact tier overrides for wide-short <=32px (header=8, footer=5,
row=7) with appropriately sized fonts
- Hole card compact layout: text stacked left, course image on right
(replaces two-column text-only layout)
- Fun facts in vegas mode: render as single-line wide cards for natural
horizontal scroll reveal instead of truncating
- Fun facts: respect user's enabled setting in vegas mode
- Fun facts: calculate scroll steps from display height instead of
hardcoding 5; increase advance interval to 3s
- Tee times: stack player names vertically on 48+ displays; compact
single-line layout on <48px
- BDF font search: add Path(__file__)-relative path so 5x7.bdf is
found regardless of working directory
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): review fixes — scroll accuracy, version bump, width guards, temp cleanup
- Fun facts scroll: compute max_scroll from actual wrapped line count
via new get_fun_fact_line_count() instead of fixed 15//visible heuristic
- Bump manifest version to 2.3.0 for new behavior/config changes
- Clamp hole card column widths: fall back to text-only layout when
card is too narrow for a useful image column (< 20px)
- Register atexit cleanup for BDF temp directory so masters_bdf_* dirs
don't accumulate in /tmp
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): scroll off-by-one, BDF cleanup logging, extract wrap helper
- Fix off-by-one in fun facts scroll: use >= instead of > so the fact
advances immediately after showing the last scroll position
- BDF temp cleanup: log on failure instead of silently swallowing,
use try/finally so cache is always cleared
- Extract _wrap_text() helper used by both get_fun_fact_line_count()
and render_fun_fact() to eliminate duplicate wrapping logic
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): don't silently drop players in stacked tee times
When player_rows < len(players), fold remaining players into the last
visible line as a comma-separated string instead of silently dropping
them. Handles edge case where player_rows == 1 by folding all players
into that single line.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): break oversized words at character boundaries in _wrap_text
Words wider than max_w (e.g. long unbroken tokens) are now split
character-by-character so no wrapped line exceeds the target width.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): Vegas scroll block sizing + defensive stale-cache guard
Addresses three user reports from Discord.
## Report 3 (JScottyR): Vegas scroll cards spanning the full panel
On a 64×64 × 5-panel chain (320×64), masters was rendering each player
card at the full panel size, so "Vegas scroll" mode showed one giant card
at a time instead of a ticker of smaller fixed-size blocks. Every other
sports scoreboard in the repo follows a fixed-block convention — football,
hockey, and baseball all default `game_card_width=128` regardless of the
physical display width, with cards constructed at that explicit size.
Fix:
- New top-level config key `scroll_card_width` (default 128, min 32,
max 256) in config_schema.json with a description referencing the
Vegas scroll ticker mode.
- `manager.py` reads it as `self._scroll_card_width` in `__init__` (and
on hot-reload), then passes `card_width=self._scroll_card_width,
card_height=self.display_height` to each `render_*` call inside
`get_vegas_content()`.
- `render_player_card()`, `render_hole_card()`, and `render_fun_fact()`
in `masters_renderer.py` now accept optional `card_width`/`card_height`
parameters. When provided, the body uses local `w`/`h`/`cw`/`ch`
variables instead of `self.width`/`self.height`, and `is_wide_short`
is recomputed per-card (so a 128×64 block on a 320×64 panel is
aspect 2.0 and uses the standard vertical-stack layout, while the
same panel's full-screen modes still use the two-column wide-short
layout for the leaderboard, schedule, etc).
- `_render_player_card_wide_short()` takes the same `w`/`h` overrides
and parameterizes every `self.width`/`self.height` reference.
- `_draw_gradient_bg()` accepts optional `width`/`height` so each render
can allocate its own image at the override size.
- Enhanced renderer overrides (`render_player_card`, `render_hole_card`)
delegate to `super()` when called with explicit card dimensions, since
the enhanced round-scores block and left-panel-plus-image layouts are
designed for full-panel rendering and don't fit at block sizes.
Verified locally at every `(parent, card)` combination in
`{(320,64), (384,64), (192,48), (128,64), (64,32)} × {(128,64), (128,48),
(80,64), (64,32)}`: every returned image's `.size` exactly matches the
override. A simulated `get_vegas_content()` on a 320×64 parent with
default `scroll_card_width=128` produces 33 cards (10 players, 18 holes,
5 facts), ALL at exactly 128×64. A 5-card scroll strip concatenated
side-by-side renders as a 640×64 image that looks like the football
scoreboard ticker pattern — headshot + name + country + score + pos/thru
per card, cleanly repeated.
## Reports 1 & 2 (Fish Man): defensive stale-cache guard
Reports 1 (`'str' object has no attribute 'tzinfo'`) and 2 (`'<=' not
supported between instances of 'float' and 'NoneType'`) are both already
fixed in PR #95 via `_rehydrate_meta()` and `_NEVER_EXPIRE` respectively,
but neither has merged to `main` yet — a user who pulled the plugin-store
update to 2.1.2 after PR #94 merged is running the exact version where
both errors fire.
Residual risk even after PR #95 / this branch ships: the disk cache file
`/var/cache/ledmatrix/masters_tournament_meta.json` persists from the
broken 2.1.2 run. The core `CacheManager` logs the `<= None` error and
returns None (cache miss) rather than re-raising, but the log noise
persists every tick until the file is overwritten by a successful fetch.
Added `MastersDataSource._safe_cache_get()` that wraps every
`cache_manager.get()` call in a try/except, treats any exception as a
cache miss, and logs a single warning per instance (not per tick)
pointing at the stale file path. Replaced all 9 `cache_manager.get()`
call sites in `masters_data.py` with the safe wrapper.
Verified with a stub `BrokenCache` that raises `TypeError('<=' not
supported...)` on every `.get()` AND a blocked network: the plugin
still initializes without crashing, returns the computed second-Thursday-
of-April fallback meta, falls back to mock leaderboard data, and logs
the stale-cache warning exactly once.
## Other
- Bumps manifest `2.2.4 → 2.2.5`.
- test_plugin_standalone.py: 45/45 still passing.
Depends on PR #95 being merged first (same branch base).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): hole card layout per effective dims + Masters wordmark assets
## Hole card layout (bundled with Vegas scroll fix)
The earlier Vegas scroll fix (2.2.5) had the enhanced `render_hole_card`
delegating to the base class when called with `card_width`/`card_height`
overrides. The base class uses a different layout (horizontal header +
centered image + footer) that's neither the user's desired "1 text column
+ image" for larger cards nor the "2 text columns, no image" for smaller
ones — so Vegas scroll hole cards looked inconsistent with the full-panel
layouts.
Rewrote the enhanced `render_hole_card` to:
1. Accept `card_width`/`card_height` directly and render at those
dimensions.
2. Choose between two layouts based on the EFFECTIVE card dimensions
(not `self.tier`), using thresholds `cw >= 96 AND ch >= 40`:
* **Large enough for image** → single left-column text
[Hole #, Name, Par, Yards] + hole image on the right, zone
badge in the bottom-right. Split into a new helper
`_render_hole_card_with_image()`.
* **Smaller canvases** → two text columns: [# / Name] | [Par /
Yards / Zone], no image. `_render_hole_card_compact()` now
also accepts `cw`/`ch` overrides.
3. `left_w` (text column width) now scales with `cw` via
`max(38, min(56, cw // 3))` so 256x64 cards get a roomier text
column than 128x48 blocks.
Verified across the full size matrix:
* 192x48 full panel → IMAGE (#12 + Golden Bell + Par 3 + 155y + map + AMEN CORNER)
* 128x64 full panel → IMAGE
* 256x64 full panel → IMAGE
* 320x64 Vegas 128x64 → IMAGE (inside a scrollable block, not full-panel)
* 320x64 Vegas 128x48 → IMAGE
* 192x48 Vegas 128x48 → IMAGE
* 320x64 Vegas 80x64 → COMPACT 2-col (too narrow for image)
* 128x32 full panel → COMPACT 2-col
* 128x32 Vegas 80x32 → COMPACT 2-col
* 64x32 full panel → COMPACT 2-col
Both layouts have the user's requested content layout:
* Image layout: one text column [Hole #, Hole Name, Par, Yards]
* Compact layout: col 1 [Hole # + Name], col 2 [Par, Yards, Zone]
## Masters wordmark assets
Extracted from `masters-icons.ttf` (IcoMoon icon font from masters.com).
The font has 143 glyphs in the U+E900–U+E98F Private Use Area, mostly
UI icons, but scanning for unusually-wide advance widths revealed two
wordmarks:
* U+E954 (5.95 em) - the iconic MASTERS wordmark with the Augusta
National contour + fairway flag + "Masters" in italic serif
* U+E95B (4.89 em) - AUGUSTA wordmark in matching italic serif
Added to `plugins/masters-tournament/assets/masters/logos/`:
* wordmark_32.png 191x32 white on transparent
* wordmark_48.png 286x49
* wordmark_64.png 381x64
* wordmark_128.png 762x128
* augusta_wordmark_32.png 156x30
* augusta_wordmark_48.png 235x45
Assets only — not yet wired into the renderer. A future change can
swap these in for the current `masters_logo_*.png` in countdown,
player card headers, etc.
Bumps manifest 2.2.5 → 2.2.6.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): pixel-perfect 5x7 BDF font for compact hole card
Previous attempts to use 5by7.regular.ttf via _load_font_sized() resulted
in washed-out anti-aliased text because PIL's TTF renderer smooths small
pixel fonts. The 5x7.bdf asset in the LEDMatrix core assets dir is a
true fixed-size bitmap font, and PIL.BdfFontFile can convert it to PIL
format for pixel-perfect 1:1 rendering.
Changes:
- Added _load_bdf_font(filename) in masters_renderer.py. Converts
BDF → PIL format once per process via tempfile.mkdtemp(), caches
the loaded ImageFont in _BDF_FONT_CACHE (keyed by filename), and
stores failure results so missing files don't re-hit the disk.
Uses a local `from PIL import BdfFontFile` import since it's only
needed by this helper.
- Enhanced renderer imports _load_bdf_font alongside _load_font_sized.
- _render_hole_card_compact() now loads 5x7.bdf for both text_font
and hole_font. The BDF renders at its native 7px height with no
anti-aliasing, so glyphs look crisp and bold. Falls back to
self.font_detail / self.font_body when the BDF file isn't on the
search path.
- The previous 4x6 code is kept in a commented block directly
underneath so we can flip fonts back in one edit if users report
issues with 5x7 on actual hardware.
Verified:
- _load_bdf_font("5x7.bdf") returns a valid ImageFont, 'Ag' bbox
height exactly 7px, second call hits the cache (same object id).
- _load_bdf_font("nonexistent.bdf") returns None cleanly.
- Render matrix across full panels (64x32, 128x32, 128x48, 128x64,
192x48, 256x64) and Vegas scroll blocks (128x64, 128x48, 128x32,
80x32) all produce expected layouts:
* ch >= 48 → IMAGE layout (left-panel text + hole image, unchanged)
* ch < 48 → COMPACT 2-col (# + Name left, Par + Yards right)
- Compact layouts now render with pixel-perfect 5x7 glyphs; the
narrower-per-glyph 5x7 font also lets "AMEN CORNER" fit fully on
a 128-wide compact card where 4x6 was truncating to "AMEN COR".
- test_plugin_standalone.py: 45/45 passing.
Note: 5x7.bdf is NOT copied into the plugin repo — it already lives
in the core LEDMatrix assets dir which the plugin's FONT_SEARCH_DIRS
already knows about. If a future deployment doesn't ship the core
fonts, the fallback to self.font_detail keeps the hole card working.
Bumps manifest 2.2.6 → 2.2.7.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): layout fixes for 32px-height displays (128x32, 256x32)
Addresses multiple layout issues reported on 2-chain and 4-chain 64x32
displays where the large tier's vertical measurements overflowed 32px.
- Add compact tier overrides for wide-short <=32px (header=8, footer=5,
row=7) with appropriately sized fonts
- Hole card compact layout: text stacked left, course image on right
(replaces two-column text-only layout)
- Fun facts in vegas mode: render as single-line wide cards for natural
horizontal scroll reveal instead of truncating
- Fun facts: respect user's enabled setting in vegas mode
- Fun facts: calculate scroll steps from display height instead of
hardcoding 5; increase advance interval to 3s
- Tee times: stack player names vertically on 48+ displays; compact
single-line layout on <48px
- BDF font search: add Path(__file__)-relative path so 5x7.bdf is
found regardless of working directory
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): review fixes — scroll accuracy, version bump, width guards, temp cleanup
- Fun facts scroll: compute max_scroll from actual wrapped line count
via new get_fun_fact_line_count() instead of fixed 15//visible heuristic
- Bump manifest version to 2.3.0 for new behavior/config changes
- Clamp hole card column widths: fall back to text-only layout when
card is too narrow for a useful image column (< 20px)
- Register atexit cleanup for BDF temp directory so masters_bdf_* dirs
don't accumulate in /tmp
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): scroll off-by-one, BDF cleanup logging, extract wrap helper
- Fix off-by-one in fun facts scroll: use >= instead of > so the fact
advances immediately after showing the last scroll position
- BDF temp cleanup: log on failure instead of silently swallowing,
use try/finally so cache is always cleared
- Extract _wrap_text() helper used by both get_fun_fact_line_count()
and render_fun_fact() to eliminate duplicate wrapping logic
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): don't silently drop players in stacked tee times
When player_rows < len(players), fold remaining players into the last
visible line as a comma-separated string instead of silently dropping
them. Handles edge case where player_rows == 1 by folding all players
into that single line.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): break oversized words at character boundaries in _wrap_text
Words wider than max_w (e.g. long unbroken tokens) are now split
character-by-character so no wrapped line exceeds the target width.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(calendar): use start-of-today for event query so already-started events appear
The API query used timeMin=now (current UTC instant), which excluded any
event that had already started earlier in the day. Changing timeMin to
midnight of the current day (in the user's configured timezone) ensures
today's events are always visible, and upcoming events still appear as
fallback when today has none.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address PR review findings across calendar and masters plugins
- calendar: bump API version to 1.0.1 for the timeMin bug-fix
- calendar: hoist time_min computation above the calendar loop so all
calendars query with the same consistent time boundary
- masters: fix last_updated in manifest.json to match 2.3.0 release date
- masters: flush current_line before character-breaking an oversized word
in _wrap_text so previous text isn't glued to the broken characters
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(calendar): remove unrelated masters changes from calendar branch
Revert masters-tournament manifest and renderer changes that were
incorrectly included in this calendar-only branch. Those fixes will
be applied on a separate branch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
/sports/golf/pga/leaderboard(404), so the plugin was silently serving mock data for the 2026 Masters. Repointed to the working/sports/golf/leaderboardendpoint, plussite.web.api.espn.com/.../common/v3/.../athletes/{id}+/overviewfor player details.events[0].date), with a computed-second-Thursday-of-April fallback for off-season. Renderer also gained a sub-hour branch so the final hour ticks down as "{m}mMINUTES TO GO" instead of jumping straight from hours to "NOW".get_tournament_phase/get_detailed_phase) now acceptsstart_date/end_dateand computes phase by comparison, so mode rotation and cache TTL follow the real tournament window. TTL drops to 30s during live play.status.position.displayName(top-levelpositionisnull);_calculate_score_to_parguards"-"placeholders;_extract_round_scoresskips pre-roundvalue=0.0 / displayValue="-"entries; country codes are pulled from the flag URL path (.../countries/500/usa.png→USA); headshot URLs fall back to the ESPN CDN template built fromplayer_id.fetch_player_details()was a stub returningNone. Now hits both the athlete endpoint (bio: age, height, weight, college, birth place, flag, headshot) and/overview(season stats +seasonRankings.categories).fetch_schedule()no longer calls the dead/sports/golf/pga/scheduleendpoint — tee-time pairings are parsed out of the leaderboard payload we already fetch, grouped by identicalstatus.teeTime.plugins.json).Test plan
Validated live against ESPN for the 2026 Masters on 2026-04-09:
fetch_leaderboard()— 91/91 real players, all withplayer_id, country code, and headshot URLfetch_tournament_meta()—name=Masters Tournament,start=2026-04-09 04:00 UTC,end=2026-04-12 04:00 UTC,status=in,round=1,is_masters=Truefetch_schedule()— 31 tee-time pairings, 91 players, sorted by startTimefetch_player_details("9478")— full Scheffler bio + 6 stats + 9 season rankings_get_cache_ttl()during live tournament → 30sget_tournament_phase/get_detailed_phasewith live meta →tournament/tournament-livecalculate_tournament_countdown(live_start)→0/0/0→ renders "NOW"calculate_tournament_countdown(now + 45m)→0/0/44→ renders "44m MINUTES TO GO"python test_plugin_standalone.py— all greenupdate_interval=30, watch the leaderboard refresh and phase transition through the 4-day tournament (requires hardware — cannot be validated in this sandbox)🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements