feat(baseball): full scorebug rendering with baseball-specific elements#27
feat(baseball): full scorebug rendering with baseball-specific elements#27ChuckBuilds merged 6 commits intomainfrom
Conversation
…ements Consolidate v2.5 baseball scorebug rendering into manager.py with dedicated display methods for live, recent, and upcoming games. Live games now show inning indicator (▲/▼), base diamonds, outs circles, balls-strikes count, and team:score layout. Standardize data extraction to flat dict format matching other sports plugins. Update scroll mode game_renderer.py with matching baseball elements. Remove unused scorebug_renderer.py. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📝 WalkthroughWalkthroughFlattened game model and new rendering pipeline: legacy scorebug renderer removed, game rendering moved to Changes
Sequence DiagramsequenceDiagram
participant Manager as Manager
participant Normalizer as Data Normalization
participant GameRenderer as GameRenderer
participant LogoMgr as Logo Manager
participant OddsMgr as Odds Manager
participant Rankings as Rankings Manager
participant Display as Display System
rect rgba(100,150,255,0.5)
Manager->>Normalizer: Fetch & normalize raw event (ESPN/MiLB)
Normalizer->>Manager: Return flattened game dict (is_live/is_final/is_upcoming, inning, counts, etc.)
Manager->>LogoMgr: Request team logos (home/away) [optional]
LogoMgr-->>Manager: Return logo images/paths
Manager->>OddsMgr: Request odds for game [optional]
OddsMgr-->>Manager: Return odds data or nil
Manager->>Rankings: Request rankings cache [optional]
Rankings-->>Manager: Return rankings map or nil
Manager->>GameRenderer: set_rankings_cache(rankings)
Manager->>GameRenderer: render_game_card(game, game_type)
GameRenderer->>GameRenderer: Compose image (logos, inning, bases, counts, scores, odds, records)
GameRenderer-->>Manager: Return rendered Image
Manager->>Display: Update display with final image
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@plugins/baseball-scoreboard/game_renderer.py`:
- Around line 380-397: The _draw_records method currently draws records
unconditionally; update it to respect the league/config flags by reading
show_records and show_ranking from the passed game dict (e.g.,
game.get('league_config', {}) or game.get('show_records') and
game.get('show_ranking')). Only proceed to draw when show_records is truthy and,
if applicable, show_ranking is allowed (match the logic used in manager.py).
Keep the existing rendering logic in _draw_records but gate the early return
with these config checks so scroll mode obeys the user's settings.
In `@plugins/baseball-scoreboard/manager.py`:
- Around line 566-576: The current logic in manager.py that handles
status_detail/status_short sets inning_half='top' and increments inning when
'end' is present, causing "End 5th" to render as top of 6th; change the handling
in the block that reads status_detail/status_short and status.get('period') so
that when 'end' is detected you do NOT increment the period (keep inning =
status.get('period', 1)) and set a distinct inning_half value (e.g., 'end' or
'mid-end') instead of mapping it to 'top'; update downstream rendering code that
consumes inning_half to recognize the new 'end' value (or render an "E5"/"M5"
marker) rather than showing a top-arrow for finished innings.
- Around line 584-593: The current guard "if balls == 0 and strikes == 0 and
situation:" treats a valid 0-0 count as "no data" and triggers the fallback;
instead check whether the original count structure was absent/empty before
falling back to parsing situation. Change the condition to test the
presence/population of the count source (e.g., "if not count and situation:" or
"if count is None or not count and situation:") and only then attempt to parse
situation['summary'] or use situation.get('balls')/get('strikes'); keep the
existing try/except around map(int, ...) and the existing fallback assignments
to balls and strikes.
🧹 Nitpick comments (3)
plugins/baseball-scoreboard/manager.py (1)
884-1031: Substantial rendering logic duplication withgame_renderer.py.The live/recent/upcoming rendering methods here (
_display_live_game,_display_recent_game,_display_upcoming_game) duplicate nearly all of the layout, geometry, font, and drawing logic fromgame_renderer.py's_render_live_game,_render_recent_game,_render_upcoming_game. This includes the bases diamond geometry, outs circles, count display, score corners, records, and text-with-outline drawing.Consider having manager.py delegate to
GameRendererfor image generation, then simply display the returned image:def _display_live_game(self, game: Dict): renderer = GameRenderer(matrix_width, matrix_height, self.config, ...) img = renderer.render_game_card(game, 'live') self.display_manager.image = img.copy() self.display_manager.update_display()This would eliminate ~300 lines of duplicated rendering code, ensuring any visual fix or change only needs to happen in one place.
plugins/baseball-scoreboard/game_renderer.py (2)
349-358: Movedatetimeandpytzimports to module level.Importing inside the method body on every call is non-idiomatic and incurs repeated import overhead.
datetimeis always available, andpytzis already a dependency of the plugin (used inmanager.py).Proposed fix
Add at the top of the file alongside other imports:
from datetime import datetime import pytzThen simplify the exception handling:
if start_time: try: - from datetime import datetime - import pytz dt = datetime.fromisoformat(start_time.replace('Z', '+00:00')) local_tz = pytz.timezone(self.config.get('timezone', 'US/Eastern')) dt_local = dt.astimezone(local_tz) game_date = dt_local.strftime('%b %d') game_time = dt_local.strftime('%-I:%M %p') - except (ValueError, AttributeError, ImportError): + except (ValueError, AttributeError): game_time = start_time[:10] if len(start_time) > 10 else start_time
75-84:_get_logo_pathhardcodes logo directories, ignoring per-leaguelogo_dirconfig.
manager.pyrespectsleague_config.get('logo_dir', ...)for MiLB and NCAA, but this renderer uses hardcoded paths. If a user configures a customlogo_dir, scroll-mode logos will still load from the default paths.Consider reading from the game's
league_configor the renderer's config:Proposed fix
def _get_logo_path(self, league: str, team_abbrev: str) -> Path: """Get the logo path for a team based on league.""" - if league == 'mlb': - return Path("assets/sports/mlb_logos") / f"{team_abbrev}.png" - elif league == 'milb': - return Path("assets/sports/milb_logos") / f"{team_abbrev}.png" - elif league == 'ncaa_baseball': - return Path("assets/sports/ncaa_logos") / f"{team_abbrev}.png" - else: - return Path("assets/sports/mlb_logos") / f"{team_abbrev}.png" + logo_dirs = { + 'mlb': 'assets/sports/mlb_logos', + 'milb': self.config.get('milb_logo_dir', 'assets/sports/milb_logos'), + 'ncaa_baseball': self.config.get('ncaa_baseball_logo_dir', 'assets/sports/ncaa_logos'), + } + logo_dir = logo_dirs.get(league, 'assets/sports/mlb_logos') + return Path(logo_dir) / f"{team_abbrev}.png"
- Fix 'end of inning' rendering: don't increment period when 'end' is detected in status text, use distinct 'end'/'mid' inning_half values and render as E5/M5 markers instead of misleading ▲ arrows - Fix count fallback: check whether count dict is present/populated rather than testing for 0-0 values, which treated valid 0-0 counts as missing data - Fix game_renderer.py _draw_records: gate on show_records/show_ranking config flags from league_config to match manager.py behavior, so scroll mode respects user settings Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Wire up BaseballOddsManager in manager.py: import, initialize, and call fetch_odds/render_odds in all three display methods (live, recent, upcoming) when show_odds is enabled - Add _draw_dynamic_odds() to game_renderer.py for scroll mode cards, matching the pattern used by football/basketball plugins - Fix game_renderer.py _get_logo_path to read logo_dir from league_config instead of hardcoding paths, so custom logo directories (MiLB, NCAA) are respected in scroll mode - Move datetime/pytz imports to module level in game_renderer.py Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In `@plugins.json`:
- Line 299: The registry entry's "last_updated" value is out of sync: update the
"last_updated" key in the plugins.json entry for the baseball-scoreboard plugin
from "2026-02-13" to "2026-02-14" so it matches the plugin manifest's
last_updated value; ensure the registry's latest_version and last_updated fields
are kept in sync with the plugin manifest going forward.
In `@plugins/baseball-scoreboard/game_renderer.py`:
- Around line 428-483: _draw_dynamic_odds places both spread and O/U at y=0
which overlaps the status/inning row; change the drawing y to a non-conflicting
row by introducing a single vertical offset (e.g., odds_y) and use it for both
calls to _draw_text_with_outline instead of hardcoded 0; update
_draw_dynamic_odds (use symbols: _draw_dynamic_odds, _draw_text_with_outline,
favored_spread, over_under, self.display_width, self.fonts['detail']) to compute
odds_y (for example odds_y = 2 or derive from a status_row constant) and pass
(spread_x, odds_y) and (ou_x, odds_y) so spread and O/U render on their own row
and no longer collide with the status/inning text.
- Around line 440-445: The code in game_renderer (around the spread handling:
variables top_level_spread, home_spread, away_spread) treats home_spread == 0.0
as "missing" which overwrites a valid pick'em line; change the condition so only
a missing value (None) triggers assignment from top_level_spread (i.e., replace
the check "home_spread is None or home_spread == 0.0" with just "home_spread is
None"), leaving the away_spread logic (setting away_spread = -top_level_spread
when away_spread is None) unchanged.
In `@plugins/baseball-scoreboard/manager.py`:
- Around line 901-915: The _fetch_and_render_odds method currently performs
blocking network I/O via self._odds_manager.fetch_odds during rendering; move
all odds retrieval into the update() cycle and make _fetch_and_render_odds only
render already-populated odds data. Specifically, remove the fetch call from
_fetch_and_render_odds (leave only lookup and call to
self._odds_manager.render_odds using game.get('odds')), and in update() (where
games are fetched) iterate games with show_odds true (as determined by
league_config/get('show_odds', ...)) and call self._odds_manager.fetch_odds for
each game so odds are populated/cached before display; ensure you still respect
BaseOddsManager cache and timeout behavior and avoid adding blocking fetch calls
in _display_live_game, _display_recent_game, or _display_upcoming_game paths.
🧹 Nitpick comments (4)
plugins/baseball-scoreboard/game_renderer.py (2)
77-77: Use explicitOptional[Dict]for nullable parameters.
Optionalis already imported; the type hints should match PEP 484.Proposed fix
- def _get_logo_path(self, league: str, team_abbrev: str, game: Dict = None) -> Path: + def _get_logo_path(self, league: str, team_abbrev: str, game: Optional[Dict] = None) -> Path:- def _load_and_resize_logo(self, league: str, team_abbrev: str, game: Dict = None) -> Optional[Image.Image]: + def _load_and_resize_logo(self, league: str, team_abbrev: str, game: Optional[Dict] = None) -> Optional[Image.Image]:Also applies to: 94-94
485-486: Useself.logger.exceptioninstead ofself.logger.errorto capture the traceback.Per the static analysis hint (TRY400),
logging.exceptionautomatically includes the traceback, which is more useful for debugging rendering failures.Proposed fix
except Exception as e: - self.logger.error(f"Error drawing odds: {e}") + self.logger.exception(f"Error drawing odds: {e}")plugins/baseball-scoreboard/manager.py (2)
917-1070: Substantial rendering duplication betweenmanager.pyandgame_renderer.py.The live game rendering logic (bases diamond, outs circles, count, inning indicator, scores) is implemented nearly identically here and in
game_renderer.py's_render_live_game. The same applies to_render_recent_gameand_render_upcoming_game. If a rendering bug is found or the layout is adjusted, both files need synchronized changes.Consider extracting the shared rendering primitives (bases drawing, outs circles, count rendering, score layout) into a common module that both
manager.pyandgame_renderer.pycan import. This would be a meaningful maintainability improvement for future updates.
721-739: Mixingstatus_statestring checks withis_*boolean flags for the same filter logic.Lines 724, 727, 734 filter on
status_state == 'in'/'post'/'pre', while lines 729, 736 count usingg.get('is_final')/g.get('is_upcoming'). Both are derived from the same source so it's not a bug, but using the boolean flags consistently would be clearer:Proposed fix
- if mode == 'baseball_live' and status_state == 'in': + if mode == 'baseball_live' and game.get('is_live'): filtered.append(game) - elif mode == 'baseball_recent' and status_state == 'post': + elif mode == 'baseball_recent' and game.get('is_final'): recent_limit = league_config.get('recent_games_to_show', 5) recent_count = len([g for g in filtered if g.get('league') == league_key and g.get('is_final')]) if recent_count >= recent_limit: continue filtered.append(game) - elif mode == 'baseball_upcoming' and status_state == 'pre': + elif mode == 'baseball_upcoming' and game.get('is_upcoming'): upcoming_limit = league_config.get('upcoming_games_to_show', 10) upcoming_count = len([g for g in filtered if g.get('league') == league_key and g.get('is_upcoming')]) if upcoming_count >= upcoming_limit: continue filtered.append(game)
- Wire up BaseballLogoManager for auto-download of missing logos via ESPN API, with fallback to inline logo loading when unavailable - Wire up BaseballRankingsManager to fetch real team rankings (AP Top 25 etc.) from ESPN standings API, cached for 1 hour - Update _draw_records in both manager.py and game_renderer.py to show "#rank" (e.g., "#5") when show_ranking is enabled, matching the football/basketball pattern - Add _get_team_display_text helper for consistent ranking/record display logic across switch and scroll modes - Pass rankings cache through scroll_display.py to GameRenderer via set_rankings_cache() for scroll mode support - Version bump to 1.3.0 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
plugins/baseball-scoreboard/game_renderer.py (1)
97-127:⚠️ Potential issue | 🟡 MinorLogo cache key ignores
league_config.logo_dir, risking stale/wrong logos.
cache_keyisf"{league}_{team_abbrev}"(line 99), but_get_logo_pathcan resolve to different directories depending ongame['league_config']['logo_dir']. If two games for the same league/team have differentlogo_diroverrides (e.g., a custom config vs. default), the first-cached logo wins regardless.Consider incorporating the resolved
logo_dirinto the cache key:Proposed fix
def _load_and_resize_logo(self, league: str, team_abbrev: str, game: Dict = None) -> Optional[Image.Image]: """Load and resize a team logo, with caching.""" - cache_key = f"{league}_{team_abbrev}" + logo_dir = "" + if game and game.get('league_config'): + logo_dir = game['league_config'].get('logo_dir', '') + cache_key = f"{league}_{team_abbrev}_{logo_dir}" if cache_key in self._logo_cache: return self._logo_cache[cache_key]
🤖 Fix all issues with AI agents
In `@plugins/baseball-scoreboard/manager.py`:
- Around line 416-436: The _fetch_all_rankings method mutates
self._team_rankings_cache without synchronization while readers like
_get_team_display_text and _draw_records may access it concurrently; protect
this write by either acquiring the existing _games_lock around the update (wrap
the call to self._team_rankings_cache.update(...) in a with self._games_lock:
block) or perform an atomic swap: build a new dict locally (e.g., new_cache),
populate it from self._rankings_manager.fetch_rankings(...), then under
self._games_lock replace self._team_rankings_cache = new_cache; update the
_fetch_all_rankings and any callers (e.g., update) accordingly to use the
locked/atomic-swap approach.
- Around line 889-895: The Image.open(found_path) call in the logo fallback path
leaves the source file open; change it to use a context manager (with
Image.open(found_path) as src:) and inside the block call src.convert('RGBA') to
produce the logo image, then proceed with logo.thumbnail(...) and further
processing so the original file handle is closed immediately; update the code
around the logo creation in the same function that contains the found_path check
to mirror the pattern used in game_renderer.py.
- Around line 892-895: The code uses Image.Resampling.LANCZOS directly in the
logo loading path (logo.thumbnail call) which raises AttributeError on Pillow <
9.1; add a compatibility shim near the top of the file (after imports) to set a
module-level RESAMPLE_FILTER by trying Image.Resampling.LANCZOS and falling back
to Image.LANCZOS on AttributeError, then replace Image.Resampling.LANCZOS in the
logo.thumbnail call with RESAMPLE_FILTER; apply the same shim and replacement in
logo_manager.py where the same pattern occurs.
🧹 Nitpick comments (4)
plugins/baseball-scoreboard/game_renderer.py (3)
162-297: Large amount of duplicated rendering logic betweengame_renderer.pyandmanager.py.The live game rendering in this file (lines 162–297) is nearly identical to
_display_live_gameinmanager.py(lines 1011–1164) — same inning indicator logic, same bases diamond geometry, same outs circles, same score layout. The same duplication applies to recent and upcoming renderers. This means any bug fix or layout tweak must be applied in two places.Consider extracting the shared rendering logic (e.g., bases drawing, inning indicator, score layout) into common helper functions that both the manager's display path and the scroll renderer can call.
80-80: Use explicitOptionaltype annotation per PEP 484.Static analysis (RUF013) flags implicit
Optionalon lines 80 and 97. UseOptional[Dict]instead ofDict = None.Proposed fix
- def _get_logo_path(self, league: str, team_abbrev: str, game: Dict = None) -> Path: + def _get_logo_path(self, league: str, team_abbrev: str, game: Optional[Dict] = None) -> Path:- def _load_and_resize_logo(self, league: str, team_abbrev: str, game: Dict = None) -> Optional[Image.Image]: + def _load_and_resize_logo(self, league: str, team_abbrev: str, game: Optional[Dict] = None) -> Optional[Image.Image]:
509-511: Useself.logger.exceptioninstead ofself.logger.errorin the exception handler.Per static analysis (TRY400),
logging.exceptionautomatically includes the traceback, which is more appropriate inside anexceptblock.Proposed fix
except Exception as e: - self.logger.error(f"Error drawing odds: {e}") + self.logger.exception(f"Error drawing odds: {e}")plugins/baseball-scoreboard/manager.py (1)
1011-1164: Massive code duplication withgame_renderer.py.The entire live game rendering (inning indicator, bases diamond, outs circles, balls/strikes count, team scores) is duplicated between
_display_live_gamehere and_render_live_gameingame_renderer.py. Same for recent and upcoming renderers. This violates DRY — any layout fix or visual change must be applied in both places.Consider having the manager delegate to the same rendering helpers (or to the
GameRendererclass itself), rather than maintaining parallel implementations.
| logo = Image.open(found_path).convert('RGBA') | ||
| max_width = int(self.display_manager.matrix.width * 1.5) | ||
| max_height = int(self.display_manager.matrix.height * 1.5) | ||
| logo.thumbnail((max_width, max_height), Image.Resampling.LANCZOS) |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Pillow Image.Resampling.LANCZOS minimum version requirement
💡 Result:
Image.Resampling.LANCZOS requires Pillow >= 9.1.0 (released 2022-04-01), which introduced the Image.Resampling enum and mapped legacy constants like Image.LANCZOS / Image.ANTIALIAS to Image.Resampling.LANCZOS. [1]
Source: Pillow 9.1.0 release notes (Constants deprecations table). [1]
References
[1] Pillow 9.1.0 release notes (2022-04-01), “Constants” deprecations: Image.LANCZOS → Image.Resampling.LANCZOS and Image.ANTIALIAS → Image.Resampling.LANCZOS.
🏁 Script executed:
# Find project dependency files to check Pillow version requirement
find . -type f \( -name "requirements*.txt" -o -name "setup.py" -o -name "pyproject.toml" -o -name "setup.cfg" -o -name "Pipfile" \) | head -20Repository: ChuckBuilds/ledmatrix-plugins
Length of output: 919
🏁 Script executed:
# Inspect game_renderer.py lines 16-21 for the compatibility shim
fd "game_renderer.py" -x cat -n {} | head -50Repository: ChuckBuilds/ledmatrix-plugins
Length of output: 1952
🏁 Script executed:
# Inspect manager.py around lines 892-895
fd "manager.py" plugins/baseball-scoreboard/ -x wc -l {} && fd "manager.py" plugins/baseball-scoreboard/ -x sed -n '885,905p' {}Repository: ChuckBuilds/ledmatrix-plugins
Length of output: 1119
🏁 Script executed:
# Check for Image.Resampling usage across codebase
rg "Image\.Resampling" --type pyRepository: ChuckBuilds/ledmatrix-plugins
Length of output: 7104
🏁 Script executed:
# Check the baseball-scoreboard requirements to see Pillow version
cat plugins/baseball-scoreboard/requirements.txtRepository: ChuckBuilds/ledmatrix-plugins
Length of output: 403
🏁 Script executed:
# Check if logo_manager.py has the compatibility shim
fd "logo_manager.py" plugins/baseball-scoreboard/ -x head -30 {}Repository: ChuckBuilds/ledmatrix-plugins
Length of output: 805
🏁 Script executed:
# Verify if manager.py has a compatibility shim at the top
fd "manager.py" plugins/baseball-scoreboard/ -x head -100 {}Repository: ChuckBuilds/ledmatrix-plugins
Length of output: 17449
Image.Resampling.LANCZOS will raise AttributeError on Pillow < 9.1.
The project requires Pillow >= 8.0.0, which does not have Image.Resampling.LANCZOS (added in 9.1.0). Line 895 uses it directly without the compatibility fallback that game_renderer.py already implements. This will crash the logo-loading path on Pillow 8.x.
Proposed fix
Add a compatibility shim near the top of the file (after imports):
try:
RESAMPLE_FILTER = Image.Resampling.LANCZOS
except AttributeError:
RESAMPLE_FILTER = Image.LANCZOSThen on line 895:
- logo.thumbnail((max_width, max_height), Image.Resampling.LANCZOS)
+ logo.thumbnail((max_width, max_height), RESAMPLE_FILTER)Note: logo_manager.py has the same issue in two places and needs the same fix.
🤖 Prompt for AI Agents
In `@plugins/baseball-scoreboard/manager.py` around lines 892 - 895, The code uses
Image.Resampling.LANCZOS directly in the logo loading path (logo.thumbnail call)
which raises AttributeError on Pillow < 9.1; add a compatibility shim near the
top of the file (after imports) to set a module-level RESAMPLE_FILTER by trying
Image.Resampling.LANCZOS and falling back to Image.LANCZOS on AttributeError,
then replace Image.Resampling.LANCZOS in the logo.thumbnail call with
RESAMPLE_FILTER; apply the same shim and replacement in logo_manager.py where
the same pattern occurs.
…odds overlap Move blocking odds fetch from render path (_fetch_and_render_odds) to update() cycle so network I/O doesn't occur during display rendering. Fix pick'em line bug where home_spread == 0.0 was treated as missing data in both game_renderer.py and odds_manager.py. Fix odds y-position in game_renderer.py to render below the status row instead of at y=0. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…compat Use atomic swap under _games_lock for _team_rankings_cache so display threads always see a consistent snapshot. Close Image.open file handles in logo fallback path and logo_manager.py by using context managers. Add RESAMPLE_FILTER compatibility shim for Pillow < 9.1 in both manager.py and logo_manager.py. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
manager.pywith dedicated display methods for live, recent, and upcoming games_extract_game_info()) to flat dict format matching football, basketball, hockey, and soccer plugins — includes baseball-specific fields from ESPNsituationdata (inning, inning_half, balls, strikes, outs, bases_occupied)game_renderer.pywith matching baseball elements for scroll display cardsscorebug_renderer.py(was never imported — dead code)Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements
Refactor