Skip to content

fix(masters-tournament): repoint to live ESPN endpoints, fix <1d countdown#94

Merged
ChuckBuilds merged 3 commits intomainfrom
fix/masters-live-espn-endpoints
Apr 9, 2026
Merged

fix(masters-tournament): repoint to live ESPN endpoints, fix <1d countdown#94
ChuckBuilds merged 3 commits intomainfrom
fix/masters-live-espn-endpoints

Conversation

@ChuckBuilds
Copy link
Copy Markdown
Owner

@ChuckBuilds ChuckBuilds commented Apr 9, 2026

Summary

  • ESPN retired /sports/golf/pga/leaderboard (404), so the plugin was silently serving mock data for the 2026 Masters. Repointed to the working /sports/golf/leaderboard endpoint, plus site.web.api.espn.com/.../common/v3/.../athletes/{id} + /overview for player details.
  • The countdown was 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 started. Countdown target is now pulled from the live ESPN feed (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}m MINUTES TO GO" instead of jumping straight from hours to "NOW".
  • Phase detection (get_tournament_phase / get_detailed_phase) now accepts start_date/end_date and computes phase by comparison, so mode rotation and cache TTL follow the real tournament window. TTL drops to 30s during live play.
  • Leaderboard parser fixes surfaced by the real payload: position now reads status.position.displayName (top-level position is null); _calculate_score_to_par guards "-" placeholders; _extract_round_scores skips pre-round value=0.0 / displayValue="-" entries; country codes are pulled from the flag URL path (.../countries/500/usa.pngUSA); headshot URLs fall back to the ESPN CDN template built from player_id.
  • fetch_player_details() was a stub returning None. 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/schedule endpoint — tee-time pairings are parsed out of the leaderboard payload we already fetch, grouped by identical status.teeTime.
  • Manifest bump 2.0.0 → 2.1.0 (pre-commit hook auto-synced plugins.json).

Test plan

Validated live against ESPN for the 2026 Masters on 2026-04-09:

  • fetch_leaderboard()91/91 real players, all with player_id, country code, and headshot URL
  • fetch_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=True
  • fetch_schedule() — 31 tee-time pairings, 91 players, sorted by startTime
  • fetch_player_details("9478") — full Scheffler bio + 6 stats + 9 season rankings
  • _get_cache_ttl() during live tournament → 30s
  • get_tournament_phase / get_detailed_phase with live meta → tournament / tournament-live
  • calculate_tournament_countdown(live_start)0/0/0 → renders "NOW"
  • calculate_tournament_countdown(now + 45m)0/0/44 → renders "44m MINUTES TO GO"
  • Countdown PNGs rendered for all branches (days / hours / minutes / NOW) — visual check passed
  • python test_plugin_standalone.py — all green
  • Smoke test on a real LEDMatrix Pi with update_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

    • Live tournament metadata caching for accurate event dates and countdowns
    • Expanded player detail retrieval (biography, stats, headshots)
    • Plugin release advanced to version 2.1.2
  • Improvements

    • Tournament phase detection now uses actual event dates for accuracy
    • Leaderboard parsing improved (tee times, positions, scores, round/thru)
    • Countdown shows minutes-only in final hour and has safer fallbacks when metadata is missing

…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>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
Registry & Manifest
plugins.json, plugins/masters-tournament/manifest.json
Updated top-level last_updated to 2026-04-09; bumped plugin manifest version 2.0.02.1.2; added versions entries for 2.1.2, 2.1.1, 2.1.0 (released 2026-04-09).
Tournament Manager
plugins/masters-tournament/manager.py
Added cached self._tournament_meta, fetch-on-init and periodic refresh in update(), switched to metadata-driven calls to get_tournament_phase/get_detailed_phase, and changed countdown source to use cached start_date with fallback.
Data Source & Normalization
plugins/masters-tournament/masters_data.py
Major refactor: updated ESPN endpoints, added shared cache keys/HTTP headers, cache-first flows for leaderboard/schedule/meta, parse/cache tee times and tournament meta, implemented real fetch_player_details() with athlete bio/overview endpoints, strengthened normalization and TTL logic.
Phase Helpers
plugins/masters-tournament/masters_helpers.py
Signatures updated: get_tournament_phase(...) and get_detailed_phase(...) now accept optional start_date/end_date and compute phases from provided metadata-aware date windows; retain heuristics when metadata missing.
Renderer / Countdown
plugins/masters-tournament/masters_renderer.py
Refined render_countdown() to render a minutes-only final-hour state (days==0 && hours==0 && minutes>0) with label "MINUTES TO GO".
Misc (constants/signatures)
plugins/masters-tournament/...
Added/changed constants and helpers (e.g., ATHLETE_URL, ATHLETE_OVERVIEW_URL, HTTP_HEADERS, fetch_tournament_meta); no external API surface beyond manifest/version and helpers signatures.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: repointing to live ESPN endpoints and fixing sub-one-day countdown behavior. It is concise, specific, and directly reflects the primary objectives.
Docstring Coverage ✅ Passed Docstring coverage is 82.76% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/masters-live-espn-endpoints

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 50e2b7b and f7d3a57.

📒 Files selected for processing (6)
  • plugins.json
  • plugins/masters-tournament/manager.py
  • plugins/masters-tournament/manifest.json
  • plugins/masters-tournament/masters_data.py
  • plugins/masters-tournament/masters_helpers.py
  • plugins/masters-tournament/masters_renderer.py

Comment on lines +46 to +50
{
"version": "2.1.0",
"released": "2026-04-09",
"ledmatrix_min_version": "2.0.0"
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
{
"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>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
plugins/masters-tournament/masters_data.py (1)

148-158: ⚠️ Potential issue | 🟠 Major

The fallback end_date still extends into Monday morning.

When endDate is missing, start_date + timedelta(days=3) keeps Thursday's tee-off hour, and the unconditional + 23:59:59 pushes the fallback past the end of Sunday. Normalize to the final calendar day before setting 23:59:59 instead 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

📥 Commits

Reviewing files that changed from the base of the PR and between f7d3a57 and 4e883d7.

📒 Files selected for processing (3)
  • plugins.json
  • plugins/masters-tournament/manifest.json
  • plugins/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>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 | 🟡 Minor

Remove the unused _is_masters_tournament method.

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: pass swallows 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 using strict=True in zip() to catch length mismatches.

If ESPN's API shape changes such that labels and values have different lengths, the current code silently truncates data. Adding strict=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

📥 Commits

Reviewing files that changed from the base of the PR and between 4e883d7 and d972593.

📒 Files selected for processing (3)
  • plugins.json
  • plugins/masters-tournament/manifest.json
  • plugins/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"}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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).

@ChuckBuilds ChuckBuilds merged commit b1340ae into main Apr 9, 2026
1 check passed
ChuckBuilds pushed a commit that referenced this pull request Apr 9, 2026
…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>
ChuckBuilds pushed a commit that referenced this pull request Apr 9, 2026
…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>
ChuckBuilds added a commit that referenced this pull request Apr 10, 2026
…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>
ChuckBuilds added a commit that referenced this pull request Apr 10, 2026
* 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>
ChuckBuilds added a commit that referenced this pull request Apr 10, 2026
* 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>
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.

1 participant