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:
@@ -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
|
||||
```
|
||||
Reference in New Issue
Block a user