# 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 ```bash # 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: 1. **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 2. **StatusItemManager** (`Managers/StatusItemManager.swift`) - Manages the menu bar item: - Displays NHL shield icon (template image for dark/light mode) - Creates and manages the NSStatusItem 3. **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 for `UNNotificationAttachment` - 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 polls - `detectChanges(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 constant - `Date+etCalendar.swift` - Calendar configured for ET - `Date+formatDateET.swift` - ET date formatters - `Date+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 Sendable` for 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 ```