Files
rzen 57358797e1 Add playoff round view, game numbers, goal scorer notifications, standings
- 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
2026-04-18 21:51:27 -04:00

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:

  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