Skip to content

fix(masters-tournament): wide-short layouts, cache rehydration, flag cleanup, LIVE alert (2.2.1)#95

Merged
ChuckBuilds merged 4 commits intomainfrom
fix/masters-layout-v2.2.1
Apr 9, 2026
Merged

fix(masters-tournament): wide-short layouts, cache rehydration, flag cleanup, LIVE alert (2.2.1)#95
ChuckBuilds merged 4 commits intomainfrom
fix/masters-layout-v2.2.1

Conversation

@ChuckBuilds
Copy link
Copy Markdown
Owner

@ChuckBuilds ChuckBuilds commented Apr 9, 2026

Follow-up to #94, found while running 2.1.2 live on a 192×48 Raspberry Pi during the 2026 Masters. Bumps 2.1.2 → 2.2.1.

Summary

  • Disk cache round-trip fixes. The LEDMatrix core CacheManager.get() signature is max_age: int = 300 — passing None raised '<=' not supported between instances of 'float' and 'NoneType' and poisoned the logs. Introduced a _NEVER_EXPIRE = 2**31 - 1 sentinel for the four read sites that wanted "return stale if present". The cache also JSON-serializes datetimes to ISO strings, so fetch_tournament_meta was handing string dates to code that expected datetime objects — added _rehydrate_meta() that parses start_date / end_date back at the read boundary.
  • Wide-short display tier. large tier was hardcoded for 128×64 and overflowed everything on 192×48. Added is_wide_short (aspect >= 2.5 on large tier), made _configure_tier() compute max_players from actual pixel height instead of a constant, and made flag_size / headshot_size tier-aware.
  • Two-column wide-short layouts. render_leaderboard (4 players/page vs 2), render_schedule (2 pairings/row), render_field_overview (par stats left / leader right), render_player_card (headshot + text left / big centered score right), and render_live_alert (LIVE badge + player/hole on the left, big LEADER/EAGLE/etc on the right). Enhanced-renderer overrides delegate to super() when wide-short so the new layouts apply everywhere. Each method now clips against actual bounds so content can't overflow the canvas.
  • Hole card on small tier (64×32). Was pushing par/yardage off the bottom because par_y was computed from the wrapped name block instead of being anchored to the canvas. Rewrote the common layout to pin hole# to the top and par/yardage to the bottom with the name filling the middle. Added a new _render_hole_card_compact specifically for small tier that drops the (unreadable) map image and uses a two-column text layout so hole#, name, par, yardage, and zone all fit. 192×48 and 128×64 layouts unchanged.
  • Player card rotation dwell timer. _display_player_cards was advancing _player_card_index every display frame (sub-second), cycling too fast to read. Added _player_card_interval (config key player_card_duration, default 8s) matching how course-tour mode works.
  • Leaderboard polish. thru column changed from gray to white for legibility. Flag size now tier-aware (14×10 on large vs the old hardcoded 10×7).
  • Country flag assets. All 16 existing masters flag PNGs had a baked-in 1px gray border — every corner pixel was (80, 80, 80, 255). That's what the user was seeing as a "1-pixel highlight around the flags". Cropped 1px off all sides of every flag (16×10 → 14×8), regenerated USA.png programmatically as an accurate 14×9 flag with red/white stripes + navy canton + dotted pixel stars, and filled in 7 countries missing from the masters set (AUT, CHN, DEN, FIN, KOR, MEX, NZL) by copying from plugins/olympics/assets/country_flags/. Total: 23 border-free flags, up from 16.

Test plan

Verified live on devpi (Raspberry Pi at 192×48) running against the live 2026 Masters data:

  • Plugin initializes with phase: tournament against live ESPN meta
  • Refreshes every 30s with no cache errors and no 404s
  • masters_leaderboard shows 4 players per page in two columns with country flags and white thru counts
  • masters_schedule shows 2 tee-time pairings per row, formatted as "7:40 AM" (Augusta local)
  • masters_field_overview shows Players / Under / Even / Over stats on the left and the leader highlight on the right
  • masters_player_card shows headshot + name + flag + position/thru on the left with a big score on the right, and cycles through the top 5 on an 8-second dwell timer (not every frame)
  • masters_live_action (LIVE alert) fits in 48px with name + hole info on the left and the big score description on the right
  • masters_countdown renders "NOW" (tournament in progress) with the new Sunday end-of-day window keeping phase=tournament through all of Sunday Augusta-local time
  • USA flag renders as recognizable red/white stripes + navy canton with no gray border artifact
  • 64×32 hole card renders #12 / Golden / Par 3 / 15.. / zone in a 2-column text layout without clipping
  • Local matrix render: 8 sizes × 8 modes = 64 combinations all produce clean PNGs
  • python test_plugin_standalone.py — 45 passing checks, no failures
  • Soak for a full round of live play on the Pi — in progress

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Wide-short two-column layouts and a compact hole card mode for small tiers
    • New display duration settings for player cards, holes, and paginated pages
  • Improvements

    • Smarter, configurable player-card timing and pagination behavior
    • Responsive leaderboard, schedule, field overview and live-alert layouts with dynamic sizing and font handling
    • More robust cache handling and metadata rehydration
    • ASCII-safe player name handling for consistent lookups
  • Chores

    • Plugin version updated to 2.2.4 (release history extended)

…cleanup, LIVE alert rewrite

Follow-up to the PR review fixes, covering a wave of on-device issues found
while running the plugin on a 192x48 panel during the 2026 Masters.

Cache: the LEDMatrix core CacheManager.get() signature is `max_age: int = 300`
and raises on None; it also JSON-serializes datetimes to ISO strings. Four sites
passed `max_age=None` to mean "return stale" and a new fetch_tournament_meta
consumer expected datetime objects back. Added a `_NEVER_EXPIRE = 2**31 - 1`
sentinel for the cache read sites and a `_rehydrate_meta()` helper that parses
start_date/end_date back into tz-aware datetimes at the read boundary, so
_get_cache_ttl, fetch_tournament_meta, and manager's countdown/phase logic all
work against the disk-backed cache.

Display tiers: "large" (>64 wide) was hardcoded for 128x64. A 192x48 panel
inherited the same layout and overflowed every vertical stack. Introduced
`is_wide_short` (aspect >= 2.5 on large tier) and made `_configure_tier()`
compute max_players from the actual pixel budget rather than a constant, so
wide-short and standard panels both get a sensible count.

Wide-short layouts: base-class `render_leaderboard` now has a two-column
layout (4 players per page vs 2 on 192x48), `render_schedule` gets the same
treatment (2 pairings per row), `render_field_overview` puts par stats on the
left and the leader highlight on the right, and `render_player_card` puts the
headshot/name/country on the left with a big centered score on the right. The
enhanced renderer overrides delegate to super() when is_wide_short so the new
layouts take effect everywhere — trading the textured background for fitting
content on short panels. Each method now clips against actual bounds.

Render_live_alert had a 3-row vertical stack that overflowed 48-tall panels.
Added a wide-short path: LIVE header across the top, player name + hole info
stacked on the left, big "LEADER"/"EAGLE!"/etc text hugging the right edge.

Hole card on small tier (64x32) was pushing par/yardage off the bottom because
the layout computed par_y from a wrapped name block instead of anchoring to the
canvas. Rewrote the common layout to pin hole # to the top and par/yardage to
the bottom, with the name clipped to whatever's left in between. Added a new
`_render_hole_card_compact` specifically for small tier that drops the map
image (unreadable at 32px) and uses a two-column text layout so hole #, name,
par, yardage, and zone all fit without clipping. 192x48 and 128x64 layouts
unchanged.

Player card rotation was advancing every display frame (~1 FPS). Added a
dwell timer (`player_card_duration`, default 8s) so cards are actually
readable, matching how course-tour mode works.

Tee time text in the thru column was gray; changed to white for legibility.
Flag size is now tier-aware (14x10 on large tier instead of the hardcoded 10x7).

Country flags: all 16 existing masters flag PNGs had a baked-in 1px gray border
(corner pixel (80,80,80,255) on every one), which was what the user saw as a
"1 pixel highlight around the flags". Cropped 1px off all sides of every flag
(16x10 -> 14x8), regenerated USA.png programmatically as a 14x9 flag with
proper red/white stripes + navy canton + dotted pixel stars, and filled in 7
countries missing from the masters set (AUT, CHN, DEN, FIN, KOR, MEX, NZL) by
copying from the olympics plugin's country_flags.

Verified live on devpi (Raspberry Pi running at 192x48) against the live 2026
Masters data: plugin initializes with phase=tournament, refreshes every 30s,
no cache errors, no 404s, all modes rendering correctly (leaderboard shows 4
players, schedule shows 2 tee time pairings per page, player cards rotate on
an 8s timer, LIVE alert fits in 48px). 64x32 hole card now shows #12, Golden,
Par 3, 15.., and zone without clipping.

Bumps manifest 2.1.2 -> 2.2.1.

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

Plugin bumped to v2.2.4. Adds dwell-timer player-card advancement, finite-cache sentinel and cache rehydration for datetime fields, TTL safeguards, ASCII-safe name transliteration, new timing config keys, and extensive renderer/layout changes for wide-short and compact layouts.

Changes

Cohort / File(s) Summary
Version & Metadata
plugins.json, plugins/masters-tournament/manifest.json
Bumped plugin top-level version to 2.2.4; prepended versions 2.2.42.2.0 entries (released 2026-04-09) to the manifest.
Manager / Timing
plugins/masters-tournament/manager.py
Added dwell-timer state (_last_player_card_advance, _player_card_interval), changed _display_player_cards to advance index only when interval elapses, and update interval on config changes.
Data & Cache
plugins/masters-tournament/masters_data.py
Introduced _NEVER_EXPIRE sentinel, replaced max_age=None with finite max_age, added @classmethod _rehydrate_meta(...) to convert ISO date strings to tz-aware datetimes on cache reads, and hardened TTL calculations with explicit type checks.
Renderer core
plugins/masters-tournament/masters_renderer.py
Major layout refactor: wide-short detection, dynamic font loader (_load_font_sized), computed max_players from pixel budget, two-column/wide-short layouts for leaderboard/schedule/player cards/field, flag/headshot sizing guards, ascii_safe-aware jacket lookup, and updated _draw_leaderboard_row signature.
Enhanced renderer
plugins/masters-tournament/masters_renderer_enhanced.py
Delegates wide-short leaderboard/player-card rendering to core renderer, adds compact hole-card rendering for small tier, rewrites hole-card anchoring/truncation and wide-short live-alert layout.
Helpers
plugins/masters-tournament/masters_helpers.py
Added ascii_safe(text: str) -> str transliteration helper and updated format_player_name to apply ASCII-safe normalization before length/truncation.
Config schema
plugins/masters-tournament/config_schema.json
Added integer timing properties: player_card_duration, hole_display_duration, page_display_duration with defaults and bounds.

Sequence Diagram(s)

sequenceDiagram
  participant Manager as Manager
  participant Cache as Cache
  participant DataSource as DataSource
  participant Renderer as Renderer

  Manager->>Cache: read tournament meta (max_age=_NEVER_EXPIRE)
  Cache-->>Manager: cached meta (ISO strings)
  Manager->>DataSource: _rehydrate_meta(cached) -> datetimes
  DataSource-->>Manager: rehydrated meta
  Manager->>Manager: check time.time() vs _last_player_card_advance
  alt interval elapsed
    Manager->>Manager: increment player card index
  end
  Manager->>Renderer: render view (player card / page / schedule)
  Renderer-->>Manager: rendered frame
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title references 'wide-short layouts', 'cache rehydration', and 'flag cleanup' which are all significant changes present in the changeset, though it indicates version 2.2.1 while the actual version bumped is 2.2.4.

✏️ 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-layout-v2.2.1

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

🤖 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/manager.py`:
- Around line 131-134: The runtime reads top-level config keys
"player_card_duration", "hole_display_duration", and "page_display_duration"
(used to set self._player_card_interval, hole display interval, and page display
interval in manager.py), but they are missing from the top-level JSON schema;
update config_schema.json by adding these three properties to the root
"properties" object with sensible defaults (player_card_duration: 8,
hole_display_duration: 15, page_display_duration: 15), include brief
"description" text for each, and add numeric constraints (type: "number" or
"integer", minimum: 1, and a reasonable maximum like 300) so the UI exposes and
validates them. Ensure the property names match exactly the keys read in
manager.py so the UI-driven config overrides the hardcoded defaults.

In `@plugins/masters-tournament/masters_renderer.py`:
- Around line 936-961: The players slicing currently truncates the list
(players[:names_per_entry]) which drops the third golfer for wide-short layouts;
instead build players_text from the full players list and let the existing
width-clipping loop shorten the string as needed. Locate the block that computes
players_text in masters_renderer.py (referencing name_budget, names_per_entry,
format_player_name, and _text_width) and replace the sliced iteration with one
over the full players list so three-player threesomes are rendered and overflow
is handled by the clipping loop.
🪄 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: cfb0b997-d4bf-4fbf-824e-b75109fe854c

📥 Commits

Reviewing files that changed from the base of the PR and between b1340ae and 3a7fd6d.

⛔ Files ignored due to path filters (23)
  • plugins/masters-tournament/assets/masters/flags/ARG.png is excluded by !**/*.png
  • plugins/masters-tournament/assets/masters/flags/AUS.png is excluded by !**/*.png
  • plugins/masters-tournament/assets/masters/flags/AUT.png is excluded by !**/*.png
  • plugins/masters-tournament/assets/masters/flags/CAN.png is excluded by !**/*.png
  • plugins/masters-tournament/assets/masters/flags/CHN.png is excluded by !**/*.png
  • plugins/masters-tournament/assets/masters/flags/DEN.png is excluded by !**/*.png
  • plugins/masters-tournament/assets/masters/flags/ENG.png is excluded by !**/*.png
  • plugins/masters-tournament/assets/masters/flags/ESP.png is excluded by !**/*.png
  • plugins/masters-tournament/assets/masters/flags/FIJ.png is excluded by !**/*.png
  • plugins/masters-tournament/assets/masters/flags/FIN.png is excluded by !**/*.png
  • plugins/masters-tournament/assets/masters/flags/GER.png is excluded by !**/*.png
  • plugins/masters-tournament/assets/masters/flags/IRL.png is excluded by !**/*.png
  • plugins/masters-tournament/assets/masters/flags/JPN.png is excluded by !**/*.png
  • plugins/masters-tournament/assets/masters/flags/KOR.png is excluded by !**/*.png
  • plugins/masters-tournament/assets/masters/flags/MEX.png is excluded by !**/*.png
  • plugins/masters-tournament/assets/masters/flags/NIR.png is excluded by !**/*.png
  • plugins/masters-tournament/assets/masters/flags/NOR.png is excluded by !**/*.png
  • plugins/masters-tournament/assets/masters/flags/NZL.png is excluded by !**/*.png
  • plugins/masters-tournament/assets/masters/flags/RSA.png is excluded by !**/*.png
  • plugins/masters-tournament/assets/masters/flags/SCO.png is excluded by !**/*.png
  • plugins/masters-tournament/assets/masters/flags/SWE.png is excluded by !**/*.png
  • plugins/masters-tournament/assets/masters/flags/USA.png is excluded by !**/*.png
  • plugins/masters-tournament/assets/masters/flags/WAL.png is excluded by !**/*.png
📒 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_renderer.py
  • plugins/masters-tournament/masters_renderer_enhanced.py

… golfer

Two PR-review follow-ups verified against the current code.

1) config_schema.json was missing player_card_duration, hole_display_duration,
   and page_display_duration. Manager reads all three (manager.py:125,129,134
   at init, and 588-590 on config hot-reload) but since they weren't in the
   schema, the LEDMatrix web UI couldn't surface them — users had to edit JSON
   by hand to override the defaults. Added all three as root-level properties
   with type: integer, minimum: 1, maximum: 300, and descriptions that match
   the manager's defaults (8 / 15 / 15).

2) render_schedule's wide-short two-column branch was slicing
   players[:names_per_entry] with names_per_entry=2, dropping the third golfer
   of every threesome. Verified against live 2026 Masters data: 29 of 31 tee-
   time groups are threesomes, so the old code was hiding the third player in
   94% of groups on 192x48. Build players_text from the full players list
   instead and let the existing width-clipping loop shorten the string — the
   user would rather see "Kataoka, C. Ortiz, Max Ho.." (truncated but
   unambiguous) than "Kataoka, C. Ortiz" (looks like a twosome). Dropped
   names_per_entry entirely since it's no longer referenced.

Bumps manifest 2.2.1 -> 2.2.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.

🧹 Nitpick comments (3)
plugins/masters-tournament/masters_renderer.py (3)

301-307: Catch specific exceptions in _get_flag().

The bare except Exception: at Line 306 silently swallows all errors, making image-loading failures hard to debug. PIL can raise OSError, IOError, or PIL.UnidentifiedImageError—catch those specifically and consider logging the failure.

🛡️ Proposed fix to catch specific exceptions
         try:
             flag = Image.open(flag_path).convert("RGBA")
             flag.thumbnail((fw, fh), Image.Resampling.NEAREST)
             self._flag_cache[country_code] = flag
             return flag
-        except Exception:
+        except (OSError, IOError) as e:
+            self.logger.debug(f"Failed to load flag {country_code}: {e}")
             return None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/masters-tournament/masters_renderer.py` around lines 301 - 307, The
_get_flag() method currently swallows all errors with a bare except; change it
to catch specific PIL/file exceptions (e.g., PIL.UnidentifiedImageError,
OSError, IOError) around the Image.open/convert/thumbnail calls, log a
descriptive error including flag_path and country_code via the renderer's logger
(or process logger) before returning None, and keep caching logic unchanged
(update handling in _get_flag, referencing flag_path, Image.open,
Image.Resampling.NEAREST, and self._flag_cache).

552-552: Simplify redundant arithmetic in score positioning.

The expression y + (self.height - score_h) // 2 - y reduces to (self.height - score_h) // 2. While functionally correct, the + y - y is confusing and should be removed for clarity.

♻️ Simplify the calculation
-            sy = y + (self.height - score_h) // 2 - y
+            sy = (self.height - score_h) // 2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/masters-tournament/masters_renderer.py` at line 552, The score
vertical position calculation in masters_renderer.py uses redundant arithmetic:
replace the expression setting sy (currently "y + (self.height - score_h) // 2 -
y") with the simplified equivalent "(self.height - score_h) // 2" to remove the
unnecessary "+ y - y" terms; update the assignment for sy in the rendering
method where score_h and self.height are used (keep variable names sy, score_h
and self.height unchanged).

432-432: Consider replacing the hardcoded 60 with a dynamic check.

The magic number 60 for col_width might not generalize well across all wide-short layouts. A more adaptive approach would check whether there's enough space for the "thru" text plus padding.

💡 Optional refactor to compute threshold dynamically
-        if self.show_thru and col_width >= 60:
+        # Only show thru if there's room for a typical "F9" or "18" label + margin
+        min_thru_space = 20
+        if self.show_thru and col_width >= min_thru_space:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/masters-tournament/masters_renderer.py` at line 432, The condition
uses a magic number (60) to decide whether to show "thru" — replace it with a
dynamic width check: compute the required width from the actual "thru" text
length (e.g., required_width = len(self.thru_text) or len(str(self.thru))) plus
any left/right padding and spacing, then change the if to "if self.show_thru and
col_width >= required_width"; update any hardcoded padding to a named constant
or attribute so this calculation lives next to the condition (refer to
self.show_thru and col_width and the attribute or method you use to derive the
thru text).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@plugins/masters-tournament/masters_renderer.py`:
- Around line 301-307: The _get_flag() method currently swallows all errors with
a bare except; change it to catch specific PIL/file exceptions (e.g.,
PIL.UnidentifiedImageError, OSError, IOError) around the
Image.open/convert/thumbnail calls, log a descriptive error including flag_path
and country_code via the renderer's logger (or process logger) before returning
None, and keep caching logic unchanged (update handling in _get_flag,
referencing flag_path, Image.open, Image.Resampling.NEAREST, and
self._flag_cache).
- Line 552: The score vertical position calculation in masters_renderer.py uses
redundant arithmetic: replace the expression setting sy (currently "y +
(self.height - score_h) // 2 - y") with the simplified equivalent "(self.height
- score_h) // 2" to remove the unnecessary "+ y - y" terms; update the
assignment for sy in the rendering method where score_h and self.height are used
(keep variable names sy, score_h and self.height unchanged).
- Line 432: The condition uses a magic number (60) to decide whether to show
"thru" — replace it with a dynamic width check: compute the required width from
the actual "thru" text length (e.g., required_width = len(self.thru_text) or
len(str(self.thru))) plus any left/right padding and spacing, then change the if
to "if self.show_thru and col_width >= required_width"; update any hardcoded
padding to a named constant or attribute so this calculation lives next to the
condition (refer to self.show_thru and col_width and the attribute or method you
use to derive the thru text).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cdf70beb-220a-42a7-a9ac-4d0964f1257a

📥 Commits

Reviewing files that changed from the base of the PR and between 3a7fd6d and c89f195.

📒 Files selected for processing (4)
  • plugins.json
  • plugins/masters-tournament/config_schema.json
  • plugins/masters-tournament/manifest.json
  • plugins/masters-tournament/masters_renderer.py
✅ Files skipped from review due to trivial changes (3)
  • plugins.json
  • plugins/masters-tournament/config_schema.json
  • plugins/masters-tournament/manifest.json

On a 192x48 display the player card was wasting most of the canvas — a 28px
headshot floating in the top-left, small fonts, lots of dead space on the
right. User request: scale everything up to fill the available space, and
scale down gracefully on smaller/larger panels.

Rewrote the wide-short branch of render_player_card into a dedicated
_render_player_card_wide_short() with proportional sizing:

- Headshot fills the vertical (height - 2*padding), capped by width/4 so
  narrow wide-short panels (e.g. 128x48) still leave room for the name and
  score columns. On 192x48 this gives a 42px headshot; on 192x64 a 48px
  headshot; on 256x64 a 58px headshot.
- Score font scales with min(height // 2.4, width // 8, 24). On 192x48 that's
  ~20px; on 128x48 it drops to 16px so the score block doesn't eat the name
  column.
- Name uses a new _fit_name() helper that tries the biggest font (PressStart2P
  down to 4x6-font) and the biggest display form (full name → "F. LastName" →
  last name only) where the full text actually fits. Truncation is a last
  resort, not the first choice.
- POS/THRU row now clips against the text column so it doesn't bleed into
  the score block. When "THRU F" would collide, falls back to just "F".
- Green-jacket strip at the bottom when there's vertical room, with a
  shorter "xN" label if the full "xN GREEN JACKETS" won't fit.
- Uses _load_font_sized() helper (new) that loads a TTF at any pixel size.

Verified at 128x48, 192x48, 192x64, 256x64: name shows as "DeChambeau" (full
last name, sometimes full name depending on width), headshot fills most of
the height, big right-edge score with a faint separator. Non-wide-short
sizes (32x16, 64x32, 128x64) unchanged — the new layout is gated on
is_wide_short. test_plugin_standalone.py: 45/45 still passing.

Deployed to devpi (192x48) and verified live.

Bumps manifest 2.2.2 -> 2.2.3.

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.

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_renderer.py (1)

306-319: ⚠️ Potential issue | 🟡 Minor

Log flag load failures before falling back.

A bad or truncated flag asset now just disappears here with no signal, which makes the new flag pack hard to validate live. Keep the None fallback, but log the failure instead of swallowing it silently.

🛠️ Proposed fix
-        except Exception:
+        except OSError as e:
+            logger.warning("Failed to load flag %s: %s", flag_path, e)
             return None
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/masters-tournament/masters_renderer.py` around lines 306 - 319, In
_get_flag, don't silently swallow exceptions when loading a flag: catch the
exception, log the failure with context (country_code and flag_path) and the
exception details, then return None; e.g., inside the except block call
self.logger.exception(...) (or self.logger.error with exc_info=True) including
f"Failed to load flag {country_code} from {flag_path}" before returning None so
failures are visible while preserving the None fallback.
🧹 Nitpick comments (1)
plugins/masters-tournament/masters_renderer.py (1)

17-20: Memoize dynamically sized fonts.

_fit_name() and _render_player_card_wide_short() now hit _load_font_sized() several times per card render, so this path keeps reopening the same TTFs from disk. On the Pi renderer, that's unnecessary hot-path I/O.

♻️ Proposed fix
+from functools import lru_cache
 import logging
 import os
 from pathlib import Path
 from typing import Any, Dict, List, Optional, Tuple
@@
+@lru_cache(maxsize=None)
 def _load_font_sized(filename: str, size: int) -> Optional[ImageFont.ImageFont]:
     """Load a specific TTF at an arbitrary point size, returning None on failure."""
     path = _find_font_path(filename)
     if not path:
         return None

Also applies to: 116-125

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/masters-tournament/masters_renderer.py` around lines 17 - 20, The
renderer re-opens TTF files repeatedly because _fit_name() and
_render_player_card_wide_short() call _load_font_sized() many times; add a
simple memoization cache inside the module (e.g., a dict keyed by (font_path,
size)) and have _load_font_sized() check the cache before loading and store the
loaded font object after the first load; update callers (_fit_name,
_render_player_card_wide_short and any other uses around lines 116-125) to rely
on _load_font_sized() so subsequent requests reuse the cached font instead of
reopening the file.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@plugins/masters-tournament/masters_renderer.py`:
- Around line 306-319: In _get_flag, don't silently swallow exceptions when
loading a flag: catch the exception, log the failure with context (country_code
and flag_path) and the exception details, then return None; e.g., inside the
except block call self.logger.exception(...) (or self.logger.error with
exc_info=True) including f"Failed to load flag {country_code} from {flag_path}"
before returning None so failures are visible while preserving the None
fallback.

---

Nitpick comments:
In `@plugins/masters-tournament/masters_renderer.py`:
- Around line 17-20: The renderer re-opens TTF files repeatedly because
_fit_name() and _render_player_card_wide_short() call _load_font_sized() many
times; add a simple memoization cache inside the module (e.g., a dict keyed by
(font_path, size)) and have _load_font_sized() check the cache before loading
and store the loaded font object after the first load; update callers
(_fit_name, _render_player_card_wide_short and any other uses around lines
116-125) to rely on _load_font_sized() so subsequent requests reuse the cached
font instead of reopening the file.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1f6c8e25-b270-4770-8900-f2477dc499a9

📥 Commits

Reviewing files that changed from the base of the PR and between c89f195 and 6a2a479.

📒 Files selected for processing (3)
  • plugins.json
  • plugins/masters-tournament/manifest.json
  • plugins/masters-tournament/masters_renderer.py
✅ Files skipped from review due to trivial changes (1)
  • plugins/masters-tournament/manifest.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • plugins.json

…literate non-ASCII names

Three PR-review follow-ups verified against the current code before fixing.

1) _get_flag silently swallowed exceptions
   masters_renderer.py:318 had a bare `except Exception: return None` with no
   logging, so a corrupt or unreadable flag PNG was invisible in the logs.
   Now logs the failure with exc_info (including country_code and flag_path)
   via self.logger.warning(..., exc_info=True), and caches the failure in
   _flag_cache so a broken file doesn't get re-opened on every frame.
   Updated the _flag_cache annotation to Dict[str, Optional[Image.Image]].

2) _load_font_sized was called ~13 times per player card render
   _fit_name loops over ~11 (filename, size) trials and
   _render_player_card_wide_short calls twice directly, each one reading
   the TTF from disk via ImageFont.truetype(). With player cards rotating
   every 8s and a ~1 FPS display loop, that's ~100 TTF reads per dwell
   cycle just for one player card.

   Added a module-level _FONT_SIZE_CACHE dict keyed by (filename, size)
   storing the loaded ImageFont (or None for failures so repeated misses
   don't re-hit disk either). Verified with a counting wrapper around
   ImageFont.truetype: 100 hot-cache iterations of the full font trial
   list = 0 disk reads after the first pass.

3) Non-ASCII player names rendered as missing-glyph boxes
   Live 2026 Masters has 7 non-ASCII players: Højgaard (x2), García,
   Olazábal, Åberg, Välimäki, Cabrera. PressStart2P handles most of these
   correctly, but the 4x6-font (which _fit_name falls back to on narrow
   wide-short displays) has no Latin Extended glyphs — "Højgaard" rendered
   as "H[box]jgaard".

   Added ascii_safe() in masters_helpers.py that applies an explicit
   single-codepoint map for characters that don't decompose via NFKD
   (ø→o, æ→ae, ß→ss, ł→l, ð→d, þ→th, Ø/Æ/... etc), then NFKD-normalizes
   the rest and strips combining marks. "Højgaard" → "Hojgaard",
   "José María Olazábal" → "Jose Maria Olazabal". All 10 transliteration
   test cases pass.

   format_player_name now routes through ascii_safe so every renderer
   that goes through it is automatically covered. The two bypass sites
   that took raw ESPN names — _fit_name (for the biggest-font-that-fits
   candidates) and the three MULTIPLE_WINNERS.get() lookups — now
   explicitly call ascii_safe.

   Bonus bug fixed: MULTIPLE_WINNERS dict key is "Jose Maria Olazabal"
   (ASCII) but ESPN returns "José María Olazábal" so the lookup missed
   and his 2 green jackets never showed. Now renders "x2 GREEN JACKETS"
   on his player card correctly.

Verified live against the 2026 Masters on devpi (192x48): all 7 non-ASCII
players render cleanly with transliterated names, Olazábal's green jacket
count shows up, schedule pairings like "Hojgaard, V. Singh, McCa..."
display correctly. test_plugin_standalone.py: 45/45 still passing.

Bumps manifest 2.2.3 -> 2.2.4.

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.

🧹 Nitpick comments (1)
plugins/masters-tournament/masters_renderer_enhanced.py (1)

335-396: Compact hole card maximizes text legibility.

Dropping the hole map for 64×32 displays is pragmatic — the map would be illegible at that size. The two-column layout successfully shows hole number, name, par, yardage, and zone.

Note: The name truncation (lines 367-368) and zone truncation (lines 391-392) don't add ellipsis, unlike the main render_hole_card() method. This is acceptable given the extreme space constraints, but documenting this intentional difference in a brief comment would aid future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/masters-tournament/masters_renderer_enhanced.py` around lines 335 -
396, The compact hole card (_render_hole_card_compact) intentionally truncates
hole_info["name"] and zone_text without adding an ellipsis due to extreme space
constraints; please add a brief inline comment next to the name_text truncation
loop and the zone_text truncation loop explaining that omission is deliberate
(unlike render_hole_card()) to aid future maintainers, referencing the
truncation behavior for name_text and zone_text so it’s clear why no "..." is
appended.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@plugins/masters-tournament/masters_renderer_enhanced.py`:
- Around line 335-396: The compact hole card (_render_hole_card_compact)
intentionally truncates hole_info["name"] and zone_text without adding an
ellipsis due to extreme space constraints; please add a brief inline comment
next to the name_text truncation loop and the zone_text truncation loop
explaining that omission is deliberate (unlike render_hole_card()) to aid future
maintainers, referencing the truncation behavior for name_text and zone_text so
it’s clear why no "..." is appended.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9bbcf97a-5310-4d96-9538-33365166aa94

📥 Commits

Reviewing files that changed from the base of the PR and between 6a2a479 and 1c72705.

📒 Files selected for processing (5)
  • plugins.json
  • plugins/masters-tournament/manifest.json
  • plugins/masters-tournament/masters_helpers.py
  • plugins/masters-tournament/masters_renderer.py
  • plugins/masters-tournament/masters_renderer_enhanced.py
✅ Files skipped from review due to trivial changes (2)
  • plugins.json
  • plugins/masters-tournament/manifest.json

@ChuckBuilds ChuckBuilds merged commit f8e8d65 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