macOS menu bar app providing NHL game situational awareness with league-wide scoreboard, dynamic polling, notifications with team logos, and configurable display options.
6.3 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
IceGlass is a macOS menu bar application that provides NHL game situational awareness. It shows recent scores, upcoming games, and game states for all NHL teams in a yesterday/today/tomorrow window. The app fetches live data from the NHL API and displays it in a dropdown menu.
Building and Testing
Build Commands
# Generate Xcode project (after modifying project.yml)
xcodegen generate
# Build the project
xcodebuild -scheme IceGlass -configuration Debug build
# Build for release
xcodebuild -scheme IceGlass -configuration Release build
# Clean build folder
xcodebuild -scheme IceGlass clean
Running the App
Open IceGlass.xcodeproj in Xcode and run the scheme, or build and run the resulting .app bundle from the build directory.
Debug vs Release Builds
- DEBUG builds include a "Developer" menu with "Force Refresh"
Architecture
Core Services Pattern
The app uses a singleton-based architecture following the same pattern as the Ovi app:
-
MainService (
Services/MainService.swift) - Central coordinator that:- Manages a single polling timer for the scoreboard API
- Dynamically adjusts polling intervals based on game state (7s live, 180s pre-game, 600s game day, 3600s idle)
- Filters API response to yesterday/today/tomorrow window in Eastern Time
-
StatusItemManager (
Managers/StatusItemManager.swift) - Manages the menu bar item:- Displays NHL shield icon (template image for dark/light mode)
- Creates and manages the NSStatusItem
-
MenuManager (
Managers/MenuManager.swift) - Builds and updates the dropdown menu:- Displays games grouped by date (YESTERDAY/TODAY/TOMORROW headers)
- Game rows show: away/home teams, scores, game state/time
- Click opens NHL GameCenter; option-click opens NHL Videocast
- Menu updates safely (skips during mouse clicks to prevent crashes)
API Integration
Single endpoint: https://api-web.nhle.com/v1/scoreboard/now
- Returns games grouped by date with scores, covering a multi-day window
- Response includes team names, abbreviations, scores, game state, period info
ApiService (Services/ApiService.swift) - Generic URL+callback fetcher
Game States (from NHL API)
Defined in Models/GameState.swift:
- FUT (Future) - More than 30 minutes before game start
- PRE (Pre-game) - Less than 30 minutes before puck drop
- LIVE - Game in progress
- CRIT - Last 5 minutes of regulation, OT, or SO
- OVER - Soft final (game over, not yet confirmed)
- FINAL - Hard final (confirmed by arena scorers)
- OFF - Official (confirmed by NHL statisticians)
Polling Intervals
Defined in Services/PollingInterval.swift:
- idle: 3600s (1 hour) - No games today
- bootstrap: 42s - Initial app launch
- gameDay: 600s (10 minutes) - Games scheduled today but not starting soon
- preGame: 180s (3 minutes) - Game starting within 30 minutes
- liveGame: 7s - Any game currently live
- everyMinute: 60s - Intermissions, post-game
Notification System
NotificationManager (Managers/NotificationManager.swift) - Handles macOS notifications:
- Game start notifications: triggered when game state transitions from FUT/PRE to LIVE
- Goal scored notifications: triggered when a team's score increases, with scoring team's logo attached
- Team logo PNGs (80x80) bundled in
TeamLogos/directory forUNNotificationAttachment - Deduplicates via Set tracking: game IDs for starts, "gameId-awayScore-homeScore" keys for goals
- First fetch suppressed (no notifications on app startup)
- AppDelegate handles notification clicks to open URLs in browser
Change Detection (in MainService):
previousGameStates: [Int: GameSnapshot]tracks game state and scores between pollsdetectChanges(in:)compares current vs previous to fire notifications
Team Logos
Downloaded from NHL CDN via Scripts/download_team_logos.sh:
- Source:
https://assets.nhle.com/logos/nhl/svg/{TEAM}_light.svg?season={SEASON} - SVGs stored in
Assets.xcassets/TeamLogo_{TEAM}.imageset/for future UI use - PNGs (80x80) stored in
TeamLogos/as bundle resources for notification attachments - Update logos each season by running the script with the new season parameter
Settings & Persistence
AppSettings (Managers/AppSettings.swift) - UserDefaults-backed:
- Launch at login (via SMAppService)
- Selected team filter (nil = all teams)
Eastern Time Handling
NHL operates on Eastern Time. Date extensions handle timezone conversions:
Date+easternTimeZone.swift- ET timezone constantDate+etCalendar.swift- Calendar configured for ETDate+formatDateET.swift- ET date formattersDate+gameWindow.swift- Yesterday/today/tomorrow date computation in ET
Important Implementation Notes
- All services use singleton pattern with private initializers
- All singleton classes use
@unchecked Sendablefor Swift 6 strict concurrency - Menu updates are skipped during mouse clicks (
isSafe()check) to prevent crashes - Timer rescheduling includes tolerance checks to avoid unnecessary invalidation
- Weak self references used throughout to prevent retain cycles
- XcodeGen is used to generate the Xcode project from
project.yml - LSUIElement=true in Info.plist (no dock icon, menu bar only)
Dependencies
- IndieAbout (SPM) - About window UI (
https://git.rzen.dev/rzen/indie-about.git)
File Organization
IceGlass/
├── Services/ - Core business logic (MainService, ApiService, PollingInterval)
├── Managers/ - UI coordination (StatusItemManager, MenuManager, NotificationManager, AppSettings)
├── Models/ - Codable data models (ScoreboardModel, GameState, NHLTeam)
├── Extensions/ - Date/Timer/NSMenuItem/TimeInterval helpers
├── Lib/ - Utilities (IceGlassLogger, AppTerminator)
├── About/ - About window (AboutWindow with IndieAbout)
├── Assets.xcassets/ - AppIcon, NHLShield, TeamLogo_*.imageset (32 SVGs)
├── TeamLogos/ - 32 team logo PNGs (80x80, for notification attachments)
├── IceGlassApp.swift - App entry point (@main)
└── AppDelegate.swift - NSApplicationDelegate, notification click handler