fix(masters-tournament): wide-short layouts, cache rehydration, flag cleanup, LIVE alert (2.2.1)#95
Conversation
…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>
📝 WalkthroughWalkthroughPlugin 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 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
⛔ Files ignored due to path filters (23)
plugins/masters-tournament/assets/masters/flags/ARG.pngis excluded by!**/*.pngplugins/masters-tournament/assets/masters/flags/AUS.pngis excluded by!**/*.pngplugins/masters-tournament/assets/masters/flags/AUT.pngis excluded by!**/*.pngplugins/masters-tournament/assets/masters/flags/CAN.pngis excluded by!**/*.pngplugins/masters-tournament/assets/masters/flags/CHN.pngis excluded by!**/*.pngplugins/masters-tournament/assets/masters/flags/DEN.pngis excluded by!**/*.pngplugins/masters-tournament/assets/masters/flags/ENG.pngis excluded by!**/*.pngplugins/masters-tournament/assets/masters/flags/ESP.pngis excluded by!**/*.pngplugins/masters-tournament/assets/masters/flags/FIJ.pngis excluded by!**/*.pngplugins/masters-tournament/assets/masters/flags/FIN.pngis excluded by!**/*.pngplugins/masters-tournament/assets/masters/flags/GER.pngis excluded by!**/*.pngplugins/masters-tournament/assets/masters/flags/IRL.pngis excluded by!**/*.pngplugins/masters-tournament/assets/masters/flags/JPN.pngis excluded by!**/*.pngplugins/masters-tournament/assets/masters/flags/KOR.pngis excluded by!**/*.pngplugins/masters-tournament/assets/masters/flags/MEX.pngis excluded by!**/*.pngplugins/masters-tournament/assets/masters/flags/NIR.pngis excluded by!**/*.pngplugins/masters-tournament/assets/masters/flags/NOR.pngis excluded by!**/*.pngplugins/masters-tournament/assets/masters/flags/NZL.pngis excluded by!**/*.pngplugins/masters-tournament/assets/masters/flags/RSA.pngis excluded by!**/*.pngplugins/masters-tournament/assets/masters/flags/SCO.pngis excluded by!**/*.pngplugins/masters-tournament/assets/masters/flags/SWE.pngis excluded by!**/*.pngplugins/masters-tournament/assets/masters/flags/USA.pngis excluded by!**/*.pngplugins/masters-tournament/assets/masters/flags/WAL.pngis excluded by!**/*.png
📒 Files selected for processing (6)
plugins.jsonplugins/masters-tournament/manager.pyplugins/masters-tournament/manifest.jsonplugins/masters-tournament/masters_data.pyplugins/masters-tournament/masters_renderer.pyplugins/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>
There was a problem hiding this comment.
🧹 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 raiseOSError,IOError, orPIL.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 - yreduces to(self.height - score_h) // 2. While functionally correct, the+ y - yis 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 hardcoded60with a dynamic check.The magic number
60forcol_widthmight 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
📒 Files selected for processing (4)
plugins.jsonplugins/masters-tournament/config_schema.jsonplugins/masters-tournament/manifest.jsonplugins/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>
There was a problem hiding this comment.
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 | 🟡 MinorLog 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
Nonefallback, 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 NoneAlso 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
📒 Files selected for processing (3)
plugins.jsonplugins/masters-tournament/manifest.jsonplugins/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>
There was a problem hiding this comment.
🧹 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
📒 Files selected for processing (5)
plugins.jsonplugins/masters-tournament/manifest.jsonplugins/masters-tournament/masters_helpers.pyplugins/masters-tournament/masters_renderer.pyplugins/masters-tournament/masters_renderer_enhanced.py
✅ Files skipped from review due to trivial changes (2)
- plugins.json
- plugins/masters-tournament/manifest.json
…cache guard
Addresses three user reports from Discord.
## Report 3 (JScottyR): Vegas scroll cards spanning the full panel
On a 64×64 × 5-panel chain (320×64), masters was rendering each player
card at the full panel size, so "Vegas scroll" mode showed one giant card
at a time instead of a ticker of smaller fixed-size blocks. Every other
sports scoreboard in the repo follows a fixed-block convention — football,
hockey, and baseball all default `game_card_width=128` regardless of the
physical display width, with cards constructed at that explicit size.
Fix:
- New top-level config key `scroll_card_width` (default 128, min 32,
max 256) in config_schema.json with a description referencing the
Vegas scroll ticker mode.
- `manager.py` reads it as `self._scroll_card_width` in `__init__` (and
on hot-reload), then passes `card_width=self._scroll_card_width,
card_height=self.display_height` to each `render_*` call inside
`get_vegas_content()`.
- `render_player_card()`, `render_hole_card()`, and `render_fun_fact()`
in `masters_renderer.py` now accept optional `card_width`/`card_height`
parameters. When provided, the body uses local `w`/`h`/`cw`/`ch`
variables instead of `self.width`/`self.height`, and `is_wide_short`
is recomputed per-card (so a 128×64 block on a 320×64 panel is
aspect 2.0 and uses the standard vertical-stack layout, while the
same panel's full-screen modes still use the two-column wide-short
layout for the leaderboard, schedule, etc).
- `_render_player_card_wide_short()` takes the same `w`/`h` overrides
and parameterizes every `self.width`/`self.height` reference.
- `_draw_gradient_bg()` accepts optional `width`/`height` so each render
can allocate its own image at the override size.
- Enhanced renderer overrides (`render_player_card`, `render_hole_card`)
delegate to `super()` when called with explicit card dimensions, since
the enhanced round-scores block and left-panel-plus-image layouts are
designed for full-panel rendering and don't fit at block sizes.
Verified locally at every `(parent, card)` combination in
`{(320,64), (384,64), (192,48), (128,64), (64,32)} × {(128,64), (128,48),
(80,64), (64,32)}`: every returned image's `.size` exactly matches the
override. A simulated `get_vegas_content()` on a 320×64 parent with
default `scroll_card_width=128` produces 33 cards (10 players, 18 holes,
5 facts), ALL at exactly 128×64. A 5-card scroll strip concatenated
side-by-side renders as a 640×64 image that looks like the football
scoreboard ticker pattern — headshot + name + country + score + pos/thru
per card, cleanly repeated.
## Reports 1 & 2 (Fish Man): defensive stale-cache guard
Reports 1 (`'str' object has no attribute 'tzinfo'`) and 2 (`'<=' not
supported between instances of 'float' and 'NoneType'`) are both already
fixed in PR #95 via `_rehydrate_meta()` and `_NEVER_EXPIRE` respectively,
but neither has merged to `main` yet — a user who pulled the plugin-store
update to 2.1.2 after PR #94 merged is running the exact version where
both errors fire.
Residual risk even after PR #95 / this branch ships: the disk cache file
`/var/cache/ledmatrix/masters_tournament_meta.json` persists from the
broken 2.1.2 run. The core `CacheManager` logs the `<= None` error and
returns None (cache miss) rather than re-raising, but the log noise
persists every tick until the file is overwritten by a successful fetch.
Added `MastersDataSource._safe_cache_get()` that wraps every
`cache_manager.get()` call in a try/except, treats any exception as a
cache miss, and logs a single warning per instance (not per tick)
pointing at the stale file path. Replaced all 9 `cache_manager.get()`
call sites in `masters_data.py` with the safe wrapper.
Verified with a stub `BrokenCache` that raises `TypeError('<=' not
supported...)` on every `.get()` AND a blocked network: the plugin
still initializes without crashing, returns the computed second-Thursday-
of-April fallback meta, falls back to mock leaderboard data, and logs
the stale-cache warning exactly once.
## Other
- Bumps manifest `2.2.4 → 2.2.5`.
- test_plugin_standalone.py: 45/45 still passing.
Depends on PR #95 being merged first (same branch base).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…cache guard
Addresses three user reports from Discord.
## Report 3 (JScottyR): Vegas scroll cards spanning the full panel
On a 64×64 × 5-panel chain (320×64), masters was rendering each player
card at the full panel size, so "Vegas scroll" mode showed one giant card
at a time instead of a ticker of smaller fixed-size blocks. Every other
sports scoreboard in the repo follows a fixed-block convention — football,
hockey, and baseball all default `game_card_width=128` regardless of the
physical display width, with cards constructed at that explicit size.
Fix:
- New top-level config key `scroll_card_width` (default 128, min 32,
max 256) in config_schema.json with a description referencing the
Vegas scroll ticker mode.
- `manager.py` reads it as `self._scroll_card_width` in `__init__` (and
on hot-reload), then passes `card_width=self._scroll_card_width,
card_height=self.display_height` to each `render_*` call inside
`get_vegas_content()`.
- `render_player_card()`, `render_hole_card()`, and `render_fun_fact()`
in `masters_renderer.py` now accept optional `card_width`/`card_height`
parameters. When provided, the body uses local `w`/`h`/`cw`/`ch`
variables instead of `self.width`/`self.height`, and `is_wide_short`
is recomputed per-card (so a 128×64 block on a 320×64 panel is
aspect 2.0 and uses the standard vertical-stack layout, while the
same panel's full-screen modes still use the two-column wide-short
layout for the leaderboard, schedule, etc).
- `_render_player_card_wide_short()` takes the same `w`/`h` overrides
and parameterizes every `self.width`/`self.height` reference.
- `_draw_gradient_bg()` accepts optional `width`/`height` so each render
can allocate its own image at the override size.
- Enhanced renderer overrides (`render_player_card`, `render_hole_card`)
delegate to `super()` when called with explicit card dimensions, since
the enhanced round-scores block and left-panel-plus-image layouts are
designed for full-panel rendering and don't fit at block sizes.
Verified locally at every `(parent, card)` combination in
`{(320,64), (384,64), (192,48), (128,64), (64,32)} × {(128,64), (128,48),
(80,64), (64,32)}`: every returned image's `.size` exactly matches the
override. A simulated `get_vegas_content()` on a 320×64 parent with
default `scroll_card_width=128` produces 33 cards (10 players, 18 holes,
5 facts), ALL at exactly 128×64. A 5-card scroll strip concatenated
side-by-side renders as a 640×64 image that looks like the football
scoreboard ticker pattern — headshot + name + country + score + pos/thru
per card, cleanly repeated.
## Reports 1 & 2 (Fish Man): defensive stale-cache guard
Reports 1 (`'str' object has no attribute 'tzinfo'`) and 2 (`'<=' not
supported between instances of 'float' and 'NoneType'`) are both already
fixed in PR #95 via `_rehydrate_meta()` and `_NEVER_EXPIRE` respectively,
but neither has merged to `main` yet — a user who pulled the plugin-store
update to 2.1.2 after PR #94 merged is running the exact version where
both errors fire.
Residual risk even after PR #95 / this branch ships: the disk cache file
`/var/cache/ledmatrix/masters_tournament_meta.json` persists from the
broken 2.1.2 run. The core `CacheManager` logs the `<= None` error and
returns None (cache miss) rather than re-raising, but the log noise
persists every tick until the file is overwritten by a successful fetch.
Added `MastersDataSource._safe_cache_get()` that wraps every
`cache_manager.get()` call in a try/except, treats any exception as a
cache miss, and logs a single warning per instance (not per tick)
pointing at the stale file path. Replaced all 9 `cache_manager.get()`
call sites in `masters_data.py` with the safe wrapper.
Verified with a stub `BrokenCache` that raises `TypeError('<=' not
supported...)` on every `.get()` AND a blocked network: the plugin
still initializes without crashing, returns the computed second-Thursday-
of-April fallback meta, falls back to mock leaderboard data, and logs
the stale-cache warning exactly once.
## Other
- Bumps manifest `2.2.4 → 2.2.5`.
- test_plugin_standalone.py: 45/45 still passing.
Depends on PR #95 being merged first (same branch base).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…d (2.2.5) (#96) * fix(masters-tournament): Vegas scroll block sizing + defensive stale-cache guard Addresses three user reports from Discord. ## Report 3 (JScottyR): Vegas scroll cards spanning the full panel On a 64×64 × 5-panel chain (320×64), masters was rendering each player card at the full panel size, so "Vegas scroll" mode showed one giant card at a time instead of a ticker of smaller fixed-size blocks. Every other sports scoreboard in the repo follows a fixed-block convention — football, hockey, and baseball all default `game_card_width=128` regardless of the physical display width, with cards constructed at that explicit size. Fix: - New top-level config key `scroll_card_width` (default 128, min 32, max 256) in config_schema.json with a description referencing the Vegas scroll ticker mode. - `manager.py` reads it as `self._scroll_card_width` in `__init__` (and on hot-reload), then passes `card_width=self._scroll_card_width, card_height=self.display_height` to each `render_*` call inside `get_vegas_content()`. - `render_player_card()`, `render_hole_card()`, and `render_fun_fact()` in `masters_renderer.py` now accept optional `card_width`/`card_height` parameters. When provided, the body uses local `w`/`h`/`cw`/`ch` variables instead of `self.width`/`self.height`, and `is_wide_short` is recomputed per-card (so a 128×64 block on a 320×64 panel is aspect 2.0 and uses the standard vertical-stack layout, while the same panel's full-screen modes still use the two-column wide-short layout for the leaderboard, schedule, etc). - `_render_player_card_wide_short()` takes the same `w`/`h` overrides and parameterizes every `self.width`/`self.height` reference. - `_draw_gradient_bg()` accepts optional `width`/`height` so each render can allocate its own image at the override size. - Enhanced renderer overrides (`render_player_card`, `render_hole_card`) delegate to `super()` when called with explicit card dimensions, since the enhanced round-scores block and left-panel-plus-image layouts are designed for full-panel rendering and don't fit at block sizes. Verified locally at every `(parent, card)` combination in `{(320,64), (384,64), (192,48), (128,64), (64,32)} × {(128,64), (128,48), (80,64), (64,32)}`: every returned image's `.size` exactly matches the override. A simulated `get_vegas_content()` on a 320×64 parent with default `scroll_card_width=128` produces 33 cards (10 players, 18 holes, 5 facts), ALL at exactly 128×64. A 5-card scroll strip concatenated side-by-side renders as a 640×64 image that looks like the football scoreboard ticker pattern — headshot + name + country + score + pos/thru per card, cleanly repeated. ## Reports 1 & 2 (Fish Man): defensive stale-cache guard Reports 1 (`'str' object has no attribute 'tzinfo'`) and 2 (`'<=' not supported between instances of 'float' and 'NoneType'`) are both already fixed in PR #95 via `_rehydrate_meta()` and `_NEVER_EXPIRE` respectively, but neither has merged to `main` yet — a user who pulled the plugin-store update to 2.1.2 after PR #94 merged is running the exact version where both errors fire. Residual risk even after PR #95 / this branch ships: the disk cache file `/var/cache/ledmatrix/masters_tournament_meta.json` persists from the broken 2.1.2 run. The core `CacheManager` logs the `<= None` error and returns None (cache miss) rather than re-raising, but the log noise persists every tick until the file is overwritten by a successful fetch. Added `MastersDataSource._safe_cache_get()` that wraps every `cache_manager.get()` call in a try/except, treats any exception as a cache miss, and logs a single warning per instance (not per tick) pointing at the stale file path. Replaced all 9 `cache_manager.get()` call sites in `masters_data.py` with the safe wrapper. Verified with a stub `BrokenCache` that raises `TypeError('<=' not supported...)` on every `.get()` AND a blocked network: the plugin still initializes without crashing, returns the computed second-Thursday- of-April fallback meta, falls back to mock leaderboard data, and logs the stale-cache warning exactly once. ## Other - Bumps manifest `2.2.4 → 2.2.5`. - test_plugin_standalone.py: 45/45 still passing. Depends on PR #95 being merged first (same branch base). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(masters-tournament): hole card layout per effective dims + Masters wordmark assets ## Hole card layout (bundled with Vegas scroll fix) The earlier Vegas scroll fix (2.2.5) had the enhanced `render_hole_card` delegating to the base class when called with `card_width`/`card_height` overrides. The base class uses a different layout (horizontal header + centered image + footer) that's neither the user's desired "1 text column + image" for larger cards nor the "2 text columns, no image" for smaller ones — so Vegas scroll hole cards looked inconsistent with the full-panel layouts. Rewrote the enhanced `render_hole_card` to: 1. Accept `card_width`/`card_height` directly and render at those dimensions. 2. Choose between two layouts based on the EFFECTIVE card dimensions (not `self.tier`), using thresholds `cw >= 96 AND ch >= 40`: * **Large enough for image** → single left-column text [Hole #, Name, Par, Yards] + hole image on the right, zone badge in the bottom-right. Split into a new helper `_render_hole_card_with_image()`. * **Smaller canvases** → two text columns: [# / Name] | [Par / Yards / Zone], no image. `_render_hole_card_compact()` now also accepts `cw`/`ch` overrides. 3. `left_w` (text column width) now scales with `cw` via `max(38, min(56, cw // 3))` so 256x64 cards get a roomier text column than 128x48 blocks. Verified across the full size matrix: * 192x48 full panel → IMAGE (#12 + Golden Bell + Par 3 + 155y + map + AMEN CORNER) * 128x64 full panel → IMAGE * 256x64 full panel → IMAGE * 320x64 Vegas 128x64 → IMAGE (inside a scrollable block, not full-panel) * 320x64 Vegas 128x48 → IMAGE * 192x48 Vegas 128x48 → IMAGE * 320x64 Vegas 80x64 → COMPACT 2-col (too narrow for image) * 128x32 full panel → COMPACT 2-col * 128x32 Vegas 80x32 → COMPACT 2-col * 64x32 full panel → COMPACT 2-col Both layouts have the user's requested content layout: * Image layout: one text column [Hole #, Hole Name, Par, Yards] * Compact layout: col 1 [Hole # + Name], col 2 [Par, Yards, Zone] ## Masters wordmark assets Extracted from `masters-icons.ttf` (IcoMoon icon font from masters.com). The font has 143 glyphs in the U+E900–U+E98F Private Use Area, mostly UI icons, but scanning for unusually-wide advance widths revealed two wordmarks: * U+E954 (5.95 em) - the iconic MASTERS wordmark with the Augusta National contour + fairway flag + "Masters" in italic serif * U+E95B (4.89 em) - AUGUSTA wordmark in matching italic serif Added to `plugins/masters-tournament/assets/masters/logos/`: * wordmark_32.png 191x32 white on transparent * wordmark_48.png 286x49 * wordmark_64.png 381x64 * wordmark_128.png 762x128 * augusta_wordmark_32.png 156x30 * augusta_wordmark_48.png 235x45 Assets only — not yet wired into the renderer. A future change can swap these in for the current `masters_logo_*.png` in countdown, player card headers, etc. Bumps manifest 2.2.5 → 2.2.6. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(masters-tournament): pixel-perfect 5x7 BDF font for compact hole card Previous attempts to use 5by7.regular.ttf via _load_font_sized() resulted in washed-out anti-aliased text because PIL's TTF renderer smooths small pixel fonts. The 5x7.bdf asset in the LEDMatrix core assets dir is a true fixed-size bitmap font, and PIL.BdfFontFile can convert it to PIL format for pixel-perfect 1:1 rendering. Changes: - Added _load_bdf_font(filename) in masters_renderer.py. Converts BDF → PIL format once per process via tempfile.mkdtemp(), caches the loaded ImageFont in _BDF_FONT_CACHE (keyed by filename), and stores failure results so missing files don't re-hit the disk. Uses a local `from PIL import BdfFontFile` import since it's only needed by this helper. - Enhanced renderer imports _load_bdf_font alongside _load_font_sized. - _render_hole_card_compact() now loads 5x7.bdf for both text_font and hole_font. The BDF renders at its native 7px height with no anti-aliasing, so glyphs look crisp and bold. Falls back to self.font_detail / self.font_body when the BDF file isn't on the search path. - The previous 4x6 code is kept in a commented block directly underneath so we can flip fonts back in one edit if users report issues with 5x7 on actual hardware. Verified: - _load_bdf_font("5x7.bdf") returns a valid ImageFont, 'Ag' bbox height exactly 7px, second call hits the cache (same object id). - _load_bdf_font("nonexistent.bdf") returns None cleanly. - Render matrix across full panels (64x32, 128x32, 128x48, 128x64, 192x48, 256x64) and Vegas scroll blocks (128x64, 128x48, 128x32, 80x32) all produce expected layouts: * ch >= 48 → IMAGE layout (left-panel text + hole image, unchanged) * ch < 48 → COMPACT 2-col (# + Name left, Par + Yards right) - Compact layouts now render with pixel-perfect 5x7 glyphs; the narrower-per-glyph 5x7 font also lets "AMEN CORNER" fit fully on a 128-wide compact card where 4x6 was truncating to "AMEN COR". - test_plugin_standalone.py: 45/45 passing. Note: 5x7.bdf is NOT copied into the plugin repo — it already lives in the core LEDMatrix assets dir which the plugin's FONT_SEARCH_DIRS already knows about. If a future deployment doesn't ship the core fonts, the fallback to self.font_detail keeps the hole card working. Bumps manifest 2.2.6 → 2.2.7. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): Vegas scroll block sizing + defensive stale-cache guard
Addresses three user reports from Discord.
## Report 3 (JScottyR): Vegas scroll cards spanning the full panel
On a 64×64 × 5-panel chain (320×64), masters was rendering each player
card at the full panel size, so "Vegas scroll" mode showed one giant card
at a time instead of a ticker of smaller fixed-size blocks. Every other
sports scoreboard in the repo follows a fixed-block convention — football,
hockey, and baseball all default `game_card_width=128` regardless of the
physical display width, with cards constructed at that explicit size.
Fix:
- New top-level config key `scroll_card_width` (default 128, min 32,
max 256) in config_schema.json with a description referencing the
Vegas scroll ticker mode.
- `manager.py` reads it as `self._scroll_card_width` in `__init__` (and
on hot-reload), then passes `card_width=self._scroll_card_width,
card_height=self.display_height` to each `render_*` call inside
`get_vegas_content()`.
- `render_player_card()`, `render_hole_card()`, and `render_fun_fact()`
in `masters_renderer.py` now accept optional `card_width`/`card_height`
parameters. When provided, the body uses local `w`/`h`/`cw`/`ch`
variables instead of `self.width`/`self.height`, and `is_wide_short`
is recomputed per-card (so a 128×64 block on a 320×64 panel is
aspect 2.0 and uses the standard vertical-stack layout, while the
same panel's full-screen modes still use the two-column wide-short
layout for the leaderboard, schedule, etc).
- `_render_player_card_wide_short()` takes the same `w`/`h` overrides
and parameterizes every `self.width`/`self.height` reference.
- `_draw_gradient_bg()` accepts optional `width`/`height` so each render
can allocate its own image at the override size.
- Enhanced renderer overrides (`render_player_card`, `render_hole_card`)
delegate to `super()` when called with explicit card dimensions, since
the enhanced round-scores block and left-panel-plus-image layouts are
designed for full-panel rendering and don't fit at block sizes.
Verified locally at every `(parent, card)` combination in
`{(320,64), (384,64), (192,48), (128,64), (64,32)} × {(128,64), (128,48),
(80,64), (64,32)}`: every returned image's `.size` exactly matches the
override. A simulated `get_vegas_content()` on a 320×64 parent with
default `scroll_card_width=128` produces 33 cards (10 players, 18 holes,
5 facts), ALL at exactly 128×64. A 5-card scroll strip concatenated
side-by-side renders as a 640×64 image that looks like the football
scoreboard ticker pattern — headshot + name + country + score + pos/thru
per card, cleanly repeated.
## Reports 1 & 2 (Fish Man): defensive stale-cache guard
Reports 1 (`'str' object has no attribute 'tzinfo'`) and 2 (`'<=' not
supported between instances of 'float' and 'NoneType'`) are both already
fixed in PR #95 via `_rehydrate_meta()` and `_NEVER_EXPIRE` respectively,
but neither has merged to `main` yet — a user who pulled the plugin-store
update to 2.1.2 after PR #94 merged is running the exact version where
both errors fire.
Residual risk even after PR #95 / this branch ships: the disk cache file
`/var/cache/ledmatrix/masters_tournament_meta.json` persists from the
broken 2.1.2 run. The core `CacheManager` logs the `<= None` error and
returns None (cache miss) rather than re-raising, but the log noise
persists every tick until the file is overwritten by a successful fetch.
Added `MastersDataSource._safe_cache_get()` that wraps every
`cache_manager.get()` call in a try/except, treats any exception as a
cache miss, and logs a single warning per instance (not per tick)
pointing at the stale file path. Replaced all 9 `cache_manager.get()`
call sites in `masters_data.py` with the safe wrapper.
Verified with a stub `BrokenCache` that raises `TypeError('<=' not
supported...)` on every `.get()` AND a blocked network: the plugin
still initializes without crashing, returns the computed second-Thursday-
of-April fallback meta, falls back to mock leaderboard data, and logs
the stale-cache warning exactly once.
## Other
- Bumps manifest `2.2.4 → 2.2.5`.
- test_plugin_standalone.py: 45/45 still passing.
Depends on PR #95 being merged first (same branch base).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): hole card layout per effective dims + Masters wordmark assets
## Hole card layout (bundled with Vegas scroll fix)
The earlier Vegas scroll fix (2.2.5) had the enhanced `render_hole_card`
delegating to the base class when called with `card_width`/`card_height`
overrides. The base class uses a different layout (horizontal header +
centered image + footer) that's neither the user's desired "1 text column
+ image" for larger cards nor the "2 text columns, no image" for smaller
ones — so Vegas scroll hole cards looked inconsistent with the full-panel
layouts.
Rewrote the enhanced `render_hole_card` to:
1. Accept `card_width`/`card_height` directly and render at those
dimensions.
2. Choose between two layouts based on the EFFECTIVE card dimensions
(not `self.tier`), using thresholds `cw >= 96 AND ch >= 40`:
* **Large enough for image** → single left-column text
[Hole #, Name, Par, Yards] + hole image on the right, zone
badge in the bottom-right. Split into a new helper
`_render_hole_card_with_image()`.
* **Smaller canvases** → two text columns: [# / Name] | [Par /
Yards / Zone], no image. `_render_hole_card_compact()` now
also accepts `cw`/`ch` overrides.
3. `left_w` (text column width) now scales with `cw` via
`max(38, min(56, cw // 3))` so 256x64 cards get a roomier text
column than 128x48 blocks.
Verified across the full size matrix:
* 192x48 full panel → IMAGE (#12 + Golden Bell + Par 3 + 155y + map + AMEN CORNER)
* 128x64 full panel → IMAGE
* 256x64 full panel → IMAGE
* 320x64 Vegas 128x64 → IMAGE (inside a scrollable block, not full-panel)
* 320x64 Vegas 128x48 → IMAGE
* 192x48 Vegas 128x48 → IMAGE
* 320x64 Vegas 80x64 → COMPACT 2-col (too narrow for image)
* 128x32 full panel → COMPACT 2-col
* 128x32 Vegas 80x32 → COMPACT 2-col
* 64x32 full panel → COMPACT 2-col
Both layouts have the user's requested content layout:
* Image layout: one text column [Hole #, Hole Name, Par, Yards]
* Compact layout: col 1 [Hole # + Name], col 2 [Par, Yards, Zone]
## Masters wordmark assets
Extracted from `masters-icons.ttf` (IcoMoon icon font from masters.com).
The font has 143 glyphs in the U+E900–U+E98F Private Use Area, mostly
UI icons, but scanning for unusually-wide advance widths revealed two
wordmarks:
* U+E954 (5.95 em) - the iconic MASTERS wordmark with the Augusta
National contour + fairway flag + "Masters" in italic serif
* U+E95B (4.89 em) - AUGUSTA wordmark in matching italic serif
Added to `plugins/masters-tournament/assets/masters/logos/`:
* wordmark_32.png 191x32 white on transparent
* wordmark_48.png 286x49
* wordmark_64.png 381x64
* wordmark_128.png 762x128
* augusta_wordmark_32.png 156x30
* augusta_wordmark_48.png 235x45
Assets only — not yet wired into the renderer. A future change can
swap these in for the current `masters_logo_*.png` in countdown,
player card headers, etc.
Bumps manifest 2.2.5 → 2.2.6.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): pixel-perfect 5x7 BDF font for compact hole card
Previous attempts to use 5by7.regular.ttf via _load_font_sized() resulted
in washed-out anti-aliased text because PIL's TTF renderer smooths small
pixel fonts. The 5x7.bdf asset in the LEDMatrix core assets dir is a
true fixed-size bitmap font, and PIL.BdfFontFile can convert it to PIL
format for pixel-perfect 1:1 rendering.
Changes:
- Added _load_bdf_font(filename) in masters_renderer.py. Converts
BDF → PIL format once per process via tempfile.mkdtemp(), caches
the loaded ImageFont in _BDF_FONT_CACHE (keyed by filename), and
stores failure results so missing files don't re-hit the disk.
Uses a local `from PIL import BdfFontFile` import since it's only
needed by this helper.
- Enhanced renderer imports _load_bdf_font alongside _load_font_sized.
- _render_hole_card_compact() now loads 5x7.bdf for both text_font
and hole_font. The BDF renders at its native 7px height with no
anti-aliasing, so glyphs look crisp and bold. Falls back to
self.font_detail / self.font_body when the BDF file isn't on the
search path.
- The previous 4x6 code is kept in a commented block directly
underneath so we can flip fonts back in one edit if users report
issues with 5x7 on actual hardware.
Verified:
- _load_bdf_font("5x7.bdf") returns a valid ImageFont, 'Ag' bbox
height exactly 7px, second call hits the cache (same object id).
- _load_bdf_font("nonexistent.bdf") returns None cleanly.
- Render matrix across full panels (64x32, 128x32, 128x48, 128x64,
192x48, 256x64) and Vegas scroll blocks (128x64, 128x48, 128x32,
80x32) all produce expected layouts:
* ch >= 48 → IMAGE layout (left-panel text + hole image, unchanged)
* ch < 48 → COMPACT 2-col (# + Name left, Par + Yards right)
- Compact layouts now render with pixel-perfect 5x7 glyphs; the
narrower-per-glyph 5x7 font also lets "AMEN CORNER" fit fully on
a 128-wide compact card where 4x6 was truncating to "AMEN COR".
- test_plugin_standalone.py: 45/45 passing.
Note: 5x7.bdf is NOT copied into the plugin repo — it already lives
in the core LEDMatrix assets dir which the plugin's FONT_SEARCH_DIRS
already knows about. If a future deployment doesn't ship the core
fonts, the fallback to self.font_detail keeps the hole card working.
Bumps manifest 2.2.6 → 2.2.7.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): layout fixes for 32px-height displays (128x32, 256x32)
Addresses multiple layout issues reported on 2-chain and 4-chain 64x32
displays where the large tier's vertical measurements overflowed 32px.
- Add compact tier overrides for wide-short <=32px (header=8, footer=5,
row=7) with appropriately sized fonts
- Hole card compact layout: text stacked left, course image on right
(replaces two-column text-only layout)
- Fun facts in vegas mode: render as single-line wide cards for natural
horizontal scroll reveal instead of truncating
- Fun facts: respect user's enabled setting in vegas mode
- Fun facts: calculate scroll steps from display height instead of
hardcoding 5; increase advance interval to 3s
- Tee times: stack player names vertically on 48+ displays; compact
single-line layout on <48px
- BDF font search: add Path(__file__)-relative path so 5x7.bdf is
found regardless of working directory
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): review fixes — scroll accuracy, version bump, width guards, temp cleanup
- Fun facts scroll: compute max_scroll from actual wrapped line count
via new get_fun_fact_line_count() instead of fixed 15//visible heuristic
- Bump manifest version to 2.3.0 for new behavior/config changes
- Clamp hole card column widths: fall back to text-only layout when
card is too narrow for a useful image column (< 20px)
- Register atexit cleanup for BDF temp directory so masters_bdf_* dirs
don't accumulate in /tmp
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): scroll off-by-one, BDF cleanup logging, extract wrap helper
- Fix off-by-one in fun facts scroll: use >= instead of > so the fact
advances immediately after showing the last scroll position
- BDF temp cleanup: log on failure instead of silently swallowing,
use try/finally so cache is always cleared
- Extract _wrap_text() helper used by both get_fun_fact_line_count()
and render_fun_fact() to eliminate duplicate wrapping logic
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): don't silently drop players in stacked tee times
When player_rows < len(players), fold remaining players into the last
visible line as a comma-separated string instead of silently dropping
them. Handles edge case where player_rows == 1 by folding all players
into that single line.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): break oversized words at character boundaries in _wrap_text
Words wider than max_w (e.g. long unbroken tokens) are now split
character-by-character so no wrapped line exceeds the target width.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): Vegas scroll block sizing + defensive stale-cache guard
Addresses three user reports from Discord.
## Report 3 (JScottyR): Vegas scroll cards spanning the full panel
On a 64×64 × 5-panel chain (320×64), masters was rendering each player
card at the full panel size, so "Vegas scroll" mode showed one giant card
at a time instead of a ticker of smaller fixed-size blocks. Every other
sports scoreboard in the repo follows a fixed-block convention — football,
hockey, and baseball all default `game_card_width=128` regardless of the
physical display width, with cards constructed at that explicit size.
Fix:
- New top-level config key `scroll_card_width` (default 128, min 32,
max 256) in config_schema.json with a description referencing the
Vegas scroll ticker mode.
- `manager.py` reads it as `self._scroll_card_width` in `__init__` (and
on hot-reload), then passes `card_width=self._scroll_card_width,
card_height=self.display_height` to each `render_*` call inside
`get_vegas_content()`.
- `render_player_card()`, `render_hole_card()`, and `render_fun_fact()`
in `masters_renderer.py` now accept optional `card_width`/`card_height`
parameters. When provided, the body uses local `w`/`h`/`cw`/`ch`
variables instead of `self.width`/`self.height`, and `is_wide_short`
is recomputed per-card (so a 128×64 block on a 320×64 panel is
aspect 2.0 and uses the standard vertical-stack layout, while the
same panel's full-screen modes still use the two-column wide-short
layout for the leaderboard, schedule, etc).
- `_render_player_card_wide_short()` takes the same `w`/`h` overrides
and parameterizes every `self.width`/`self.height` reference.
- `_draw_gradient_bg()` accepts optional `width`/`height` so each render
can allocate its own image at the override size.
- Enhanced renderer overrides (`render_player_card`, `render_hole_card`)
delegate to `super()` when called with explicit card dimensions, since
the enhanced round-scores block and left-panel-plus-image layouts are
designed for full-panel rendering and don't fit at block sizes.
Verified locally at every `(parent, card)` combination in
`{(320,64), (384,64), (192,48), (128,64), (64,32)} × {(128,64), (128,48),
(80,64), (64,32)}`: every returned image's `.size` exactly matches the
override. A simulated `get_vegas_content()` on a 320×64 parent with
default `scroll_card_width=128` produces 33 cards (10 players, 18 holes,
5 facts), ALL at exactly 128×64. A 5-card scroll strip concatenated
side-by-side renders as a 640×64 image that looks like the football
scoreboard ticker pattern — headshot + name + country + score + pos/thru
per card, cleanly repeated.
## Reports 1 & 2 (Fish Man): defensive stale-cache guard
Reports 1 (`'str' object has no attribute 'tzinfo'`) and 2 (`'<=' not
supported between instances of 'float' and 'NoneType'`) are both already
fixed in PR #95 via `_rehydrate_meta()` and `_NEVER_EXPIRE` respectively,
but neither has merged to `main` yet — a user who pulled the plugin-store
update to 2.1.2 after PR #94 merged is running the exact version where
both errors fire.
Residual risk even after PR #95 / this branch ships: the disk cache file
`/var/cache/ledmatrix/masters_tournament_meta.json` persists from the
broken 2.1.2 run. The core `CacheManager` logs the `<= None` error and
returns None (cache miss) rather than re-raising, but the log noise
persists every tick until the file is overwritten by a successful fetch.
Added `MastersDataSource._safe_cache_get()` that wraps every
`cache_manager.get()` call in a try/except, treats any exception as a
cache miss, and logs a single warning per instance (not per tick)
pointing at the stale file path. Replaced all 9 `cache_manager.get()`
call sites in `masters_data.py` with the safe wrapper.
Verified with a stub `BrokenCache` that raises `TypeError('<=' not
supported...)` on every `.get()` AND a blocked network: the plugin
still initializes without crashing, returns the computed second-Thursday-
of-April fallback meta, falls back to mock leaderboard data, and logs
the stale-cache warning exactly once.
## Other
- Bumps manifest `2.2.4 → 2.2.5`.
- test_plugin_standalone.py: 45/45 still passing.
Depends on PR #95 being merged first (same branch base).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): hole card layout per effective dims + Masters wordmark assets
## Hole card layout (bundled with Vegas scroll fix)
The earlier Vegas scroll fix (2.2.5) had the enhanced `render_hole_card`
delegating to the base class when called with `card_width`/`card_height`
overrides. The base class uses a different layout (horizontal header +
centered image + footer) that's neither the user's desired "1 text column
+ image" for larger cards nor the "2 text columns, no image" for smaller
ones — so Vegas scroll hole cards looked inconsistent with the full-panel
layouts.
Rewrote the enhanced `render_hole_card` to:
1. Accept `card_width`/`card_height` directly and render at those
dimensions.
2. Choose between two layouts based on the EFFECTIVE card dimensions
(not `self.tier`), using thresholds `cw >= 96 AND ch >= 40`:
* **Large enough for image** → single left-column text
[Hole #, Name, Par, Yards] + hole image on the right, zone
badge in the bottom-right. Split into a new helper
`_render_hole_card_with_image()`.
* **Smaller canvases** → two text columns: [# / Name] | [Par /
Yards / Zone], no image. `_render_hole_card_compact()` now
also accepts `cw`/`ch` overrides.
3. `left_w` (text column width) now scales with `cw` via
`max(38, min(56, cw // 3))` so 256x64 cards get a roomier text
column than 128x48 blocks.
Verified across the full size matrix:
* 192x48 full panel → IMAGE (#12 + Golden Bell + Par 3 + 155y + map + AMEN CORNER)
* 128x64 full panel → IMAGE
* 256x64 full panel → IMAGE
* 320x64 Vegas 128x64 → IMAGE (inside a scrollable block, not full-panel)
* 320x64 Vegas 128x48 → IMAGE
* 192x48 Vegas 128x48 → IMAGE
* 320x64 Vegas 80x64 → COMPACT 2-col (too narrow for image)
* 128x32 full panel → COMPACT 2-col
* 128x32 Vegas 80x32 → COMPACT 2-col
* 64x32 full panel → COMPACT 2-col
Both layouts have the user's requested content layout:
* Image layout: one text column [Hole #, Hole Name, Par, Yards]
* Compact layout: col 1 [Hole # + Name], col 2 [Par, Yards, Zone]
## Masters wordmark assets
Extracted from `masters-icons.ttf` (IcoMoon icon font from masters.com).
The font has 143 glyphs in the U+E900–U+E98F Private Use Area, mostly
UI icons, but scanning for unusually-wide advance widths revealed two
wordmarks:
* U+E954 (5.95 em) - the iconic MASTERS wordmark with the Augusta
National contour + fairway flag + "Masters" in italic serif
* U+E95B (4.89 em) - AUGUSTA wordmark in matching italic serif
Added to `plugins/masters-tournament/assets/masters/logos/`:
* wordmark_32.png 191x32 white on transparent
* wordmark_48.png 286x49
* wordmark_64.png 381x64
* wordmark_128.png 762x128
* augusta_wordmark_32.png 156x30
* augusta_wordmark_48.png 235x45
Assets only — not yet wired into the renderer. A future change can
swap these in for the current `masters_logo_*.png` in countdown,
player card headers, etc.
Bumps manifest 2.2.5 → 2.2.6.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): pixel-perfect 5x7 BDF font for compact hole card
Previous attempts to use 5by7.regular.ttf via _load_font_sized() resulted
in washed-out anti-aliased text because PIL's TTF renderer smooths small
pixel fonts. The 5x7.bdf asset in the LEDMatrix core assets dir is a
true fixed-size bitmap font, and PIL.BdfFontFile can convert it to PIL
format for pixel-perfect 1:1 rendering.
Changes:
- Added _load_bdf_font(filename) in masters_renderer.py. Converts
BDF → PIL format once per process via tempfile.mkdtemp(), caches
the loaded ImageFont in _BDF_FONT_CACHE (keyed by filename), and
stores failure results so missing files don't re-hit the disk.
Uses a local `from PIL import BdfFontFile` import since it's only
needed by this helper.
- Enhanced renderer imports _load_bdf_font alongside _load_font_sized.
- _render_hole_card_compact() now loads 5x7.bdf for both text_font
and hole_font. The BDF renders at its native 7px height with no
anti-aliasing, so glyphs look crisp and bold. Falls back to
self.font_detail / self.font_body when the BDF file isn't on the
search path.
- The previous 4x6 code is kept in a commented block directly
underneath so we can flip fonts back in one edit if users report
issues with 5x7 on actual hardware.
Verified:
- _load_bdf_font("5x7.bdf") returns a valid ImageFont, 'Ag' bbox
height exactly 7px, second call hits the cache (same object id).
- _load_bdf_font("nonexistent.bdf") returns None cleanly.
- Render matrix across full panels (64x32, 128x32, 128x48, 128x64,
192x48, 256x64) and Vegas scroll blocks (128x64, 128x48, 128x32,
80x32) all produce expected layouts:
* ch >= 48 → IMAGE layout (left-panel text + hole image, unchanged)
* ch < 48 → COMPACT 2-col (# + Name left, Par + Yards right)
- Compact layouts now render with pixel-perfect 5x7 glyphs; the
narrower-per-glyph 5x7 font also lets "AMEN CORNER" fit fully on
a 128-wide compact card where 4x6 was truncating to "AMEN COR".
- test_plugin_standalone.py: 45/45 passing.
Note: 5x7.bdf is NOT copied into the plugin repo — it already lives
in the core LEDMatrix assets dir which the plugin's FONT_SEARCH_DIRS
already knows about. If a future deployment doesn't ship the core
fonts, the fallback to self.font_detail keeps the hole card working.
Bumps manifest 2.2.6 → 2.2.7.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): layout fixes for 32px-height displays (128x32, 256x32)
Addresses multiple layout issues reported on 2-chain and 4-chain 64x32
displays where the large tier's vertical measurements overflowed 32px.
- Add compact tier overrides for wide-short <=32px (header=8, footer=5,
row=7) with appropriately sized fonts
- Hole card compact layout: text stacked left, course image on right
(replaces two-column text-only layout)
- Fun facts in vegas mode: render as single-line wide cards for natural
horizontal scroll reveal instead of truncating
- Fun facts: respect user's enabled setting in vegas mode
- Fun facts: calculate scroll steps from display height instead of
hardcoding 5; increase advance interval to 3s
- Tee times: stack player names vertically on 48+ displays; compact
single-line layout on <48px
- BDF font search: add Path(__file__)-relative path so 5x7.bdf is
found regardless of working directory
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): review fixes — scroll accuracy, version bump, width guards, temp cleanup
- Fun facts scroll: compute max_scroll from actual wrapped line count
via new get_fun_fact_line_count() instead of fixed 15//visible heuristic
- Bump manifest version to 2.3.0 for new behavior/config changes
- Clamp hole card column widths: fall back to text-only layout when
card is too narrow for a useful image column (< 20px)
- Register atexit cleanup for BDF temp directory so masters_bdf_* dirs
don't accumulate in /tmp
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): scroll off-by-one, BDF cleanup logging, extract wrap helper
- Fix off-by-one in fun facts scroll: use >= instead of > so the fact
advances immediately after showing the last scroll position
- BDF temp cleanup: log on failure instead of silently swallowing,
use try/finally so cache is always cleared
- Extract _wrap_text() helper used by both get_fun_fact_line_count()
and render_fun_fact() to eliminate duplicate wrapping logic
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): don't silently drop players in stacked tee times
When player_rows < len(players), fold remaining players into the last
visible line as a comma-separated string instead of silently dropping
them. Handles edge case where player_rows == 1 by folding all players
into that single line.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(masters-tournament): break oversized words at character boundaries in _wrap_text
Words wider than max_w (e.g. long unbroken tokens) are now split
character-by-character so no wrapped line exceeds the target width.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(calendar): use start-of-today for event query so already-started events appear
The API query used timeMin=now (current UTC instant), which excluded any
event that had already started earlier in the day. Changing timeMin to
midnight of the current day (in the user's configured timezone) ensures
today's events are always visible, and upcoming events still appear as
fallback when today has none.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address PR review findings across calendar and masters plugins
- calendar: bump API version to 1.0.1 for the timeMin bug-fix
- calendar: hoist time_min computation above the calendar loop so all
calendars query with the same consistent time boundary
- masters: fix last_updated in manifest.json to match 2.3.0 release date
- masters: flush current_line before character-breaking an oversized word
in _wrap_text so previous text isn't glued to the broken characters
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(calendar): remove unrelated masters changes from calendar branch
Revert masters-tournament manifest and renderer changes that were
incorrectly included in this calendar-only branch. Those fixes will
be applied on a separate branch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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
CacheManager.get()signature ismax_age: int = 300— passingNoneraised'<=' not supported between instances of 'float' and 'NoneType'and poisoned the logs. Introduced a_NEVER_EXPIRE = 2**31 - 1sentinel for the four read sites that wanted "return stale if present". The cache also JSON-serializes datetimes to ISO strings, sofetch_tournament_metawas handing string dates to code that expecteddatetimeobjects — added_rehydrate_meta()that parsesstart_date/end_dateback at the read boundary.largetier was hardcoded for 128×64 and overflowed everything on 192×48. Addedis_wide_short(aspect >= 2.5on large tier), made_configure_tier()computemax_playersfrom actual pixel height instead of a constant, and madeflag_size/headshot_sizetier-aware.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), andrender_live_alert(LIVE badge + player/hole on the left, big LEADER/EAGLE/etc on the right). Enhanced-renderer overrides delegate tosuper()when wide-short so the new layouts apply everywhere. Each method now clips against actual bounds so content can't overflow the canvas.par_ywas 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_compactspecifically 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._display_player_cardswas advancing_player_card_indexevery display frame (sub-second), cycling too fast to read. Added_player_card_interval(config keyplayer_card_duration, default 8s) matching how course-tour mode works.thrucolumn changed from gray to white for legibility. Flag size now tier-aware (14×10 on large vs the old hardcoded 10×7).(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), regeneratedUSA.pngprogrammatically 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 fromplugins/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:
phase: tournamentagainst live ESPN metamasters_leaderboardshows 4 players per page in two columns with country flags and white thru countsmasters_scheduleshows 2 tee-time pairings per row, formatted as "7:40 AM" (Augusta local)masters_field_overviewshows Players / Under / Even / Over stats on the left and the leader highlight on the rightmasters_player_cardshows 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 rightmasters_countdownrenders "NOW" (tournament in progress) with the new Sunday end-of-day window keeping phase=tournament through all of Sunday Augusta-local timepython test_plugin_standalone.py— 45 passing checks, no failures🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements
Chores