Initial commit: IceGlass NHL game tracker

macOS menu bar app providing NHL game situational awareness with
league-wide scoreboard, dynamic polling, notifications with team
logos, and configurable display options.
This commit is contained in:
2026-04-13 21:44:08 -04:00
commit 8f8f8b2755
158 changed files with 2752 additions and 0 deletions
+146
View File
@@ -0,0 +1,146 @@
# 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
```