57358797e1
- Fetch NHL standings and surface league/season game counts in the menu bar
- Prefix regular-season rows with the league-wide game number (from gameId)
- New ROUND section shows each active playoff series (matchup, series score,
next game number + time) derived from /v1/playoff-bracket; rows always open
the NHL series page so completed series remain clickable
- Goal notifications include scorer sweater, abbreviated name, and strength
(PPG/SHG/EN), resolved via /v1/gamecenter/{id}/play-by-play
- Drop the per-team filter submenu and NHLTeam enum
- Regenerate AppIcon with the full 10-size macOS set (alpha preserved) so
notifications render the app icon correctly; rename the iOS marketing PNG
to icon-ios-1024.png
- gitignore .claude/ local tooling settings
147 lines
6.3 KiB
Markdown
147 lines
6.3 KiB
Markdown
# 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, StandingsModel, GameState)
|
|
├── 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
|
|
```
|