diff options
| author | ivar <i@oiee.no> | 2026-05-07 01:24:28 +0200 |
|---|---|---|
| committer | ivar <i@oiee.no> | 2026-05-07 01:24:28 +0200 |
| commit | 6eb17a18e901e2d7faa219d7e5a79083a5891dc9 (patch) | |
| tree | 3d0796e1e567864dfdf7c675f7e8a5a40fb51a95 /AGENTS.md | |
| parent | 4fb690150b77afced6453e6bdb14cc4cf00d5305 (diff) | |
| download | solverv-6eb17a18e901e2d7faa219d7e5a79083a5891dc9.tar.xz solverv-6eb17a18e901e2d7faa219d7e5a79083a5891dc9.zip | |
Diffstat (limited to 'AGENTS.md')
| -rw-r--r-- | AGENTS.md | 151 |
1 files changed, 151 insertions, 0 deletions
diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..da711d2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,151 @@ +# Solverv — Agent Reference + +Norwegian solstice and equinox countdown app for iOS, with a WidgetKit extension. +Built with SwiftUI, SwiftData, CoreLocation, and WidgetKit. + +## Repository layout + +``` +Solverv/ Main app target + SolvervApp.swift @main entry point, LocationManager + ContentView.swift Primary UI (struct is ContentView; file header says InfoScreenView) + Item.swift SwiftData @Model stub (Xcode template remnant, not used by UI) + Assets.xcassets/ + +Shared/ Code compiled into both the app and widget targets + Models/ + SolsticeData.swift Singleton; hardcoded events 2025–2030; query API + SolsticeEvent.swift Identifiable+Codable struct; daysUntil(), localDateTime() + Season.swift Enum: spring/summer/autumn/winter; color, assetName, fromDate() + Utilities/ + SunTimes.swift NOAA solar algorithm (zenith 90.833°) + AppGroupManager.swift App Group bridge (location + sun times) + +Solsnu.Widget/ WidgetKit extension + Solsnu_Widget.swift Provider, SolvervEntry, Solsnu_Widget config, SolvervDef view-model + Views/ + SmallWidgetView.swift + MediumWidgetView.swift + +SolverVTests/ XCTest suite + Models/SolsticeDataTests.swift + Utilities/SunTimesTests.swift, AppGroupManagerTests.swift + +Solverv.xcodeproj/ Xcode project; no SPM Package.swift, no Podfile +``` + +## Key types and contracts + +### `SolsticeData` (Shared/Models/) +Singleton (`SolsticeData.shared`). Stores hardcoded UTC events from 2025 through 2030, +sorted ascending. All query methods take an optional `now: Date` (defaults to `Date()`). + +- `nextEvent(from:) -> SolsticeEvent?` — first event with `date > now` +- `upcomingEvents(count:from:) -> [SolsticeEvent]` — next N events +- `progressToNextEvent(from:) -> (elapsed: Int, total: Int)?` — day counts between + the previous and next event; used for progress bars + +**Coverage ends at 2030.** Agents adding future events must follow the existing pattern: +`SolsticeEvent(name: "<Norwegian name> <year>", date: SolsticeData.dateFromUTC(...), season: .<season>)`. + +### `SolsticeEvent` (Shared/Models/) +`Identifiable, Codable`. UUID is generated at init (not stable across serialization rounds). + +- `daysUntil(from:) -> Int` — calendar-day difference, floored at 0 +- `localDateTime() -> Date` — **known bug**: adds UTC offset as raw seconds into + DateComponents rather than using a proper timezone-aware conversion. Do not expand + callers of this method without fixing it first. + +### `Season` (Shared/Models/) +`fromDate(_:)` uses meteorological (month-based) seasons, not astronomical ones. +`assetName` returns `"Season\(displayName)"` — maps to named color sets in Assets.xcassets. + +### `SunTimes` (Shared/Utilities/) +Pure value type. Implements the NOAA solar position algorithm (zenith = 90.833° to +account for atmospheric refraction + solar disk radius). Returns `nil` for polar +night/midnight-sun conditions (cosH out of [-1, 1] range). No caching; callers +instantiate per-date. + +```swift +let st = SunTimes(latitude: lat, longitude: lon, date: date) +let sunrise = st.sunrise() // Date? in device local time +let sunset = st.sunset() // Date? in device local time +``` + +### `AppGroupManager` (Shared/Utilities/) +Singleton (`AppGroupManager.shared`). App Group ID: `group.com.ivarlovlie.solverv`. +Bridges data between the app and widget via `UserDefaults(suiteName:)`. + +Two stored payloads: +- `"userLocation"` → `UserLocation` (lat, lon, ISO8601 timestamp, isDefaultLocation) +- `"sunTimes"` → `SunTimes` (date string, ISO8601 sunrise/sunset strings, timestamp) + +The widget treats location as stale after 24 hours. Sun times are cached separately +but the widget recomputes them from the stored location rather than reading the cached +sun times struct (the `"sunTimes"` key is written by the app but not read by the widget). + +### `SolvervDef` (Solsnu.Widget/Solsnu_Widget.swift) +View-model struct used by widget views. Wraps `SolsticeData` and `SunTimes` queries. +`init(utcString:)` force-unwraps the date parse — acknowledged in a comment, only used +in preview code. Do not use this initializer in production paths. + +### `LocationManager` (Solverv/SolvervApp.swift) +`CLLocationManagerDelegate`, ObservableObject. Requests `whenInUse` authorization, +calls `startUpdatingLocation()`, saves to `AppGroupManager` on first fix, then stops. +Does not handle permission denial or restricted state beyond silent no-op. + +## Data flow + +``` +[CLLocationManager] --location--> [LocationManager] + | + AppGroupManager.saveLocation() + AppGroupManager.saveSunTimes() + | + UserDefaults (App Group) + | + +------------------+------------------+ + | | + [ContentView] [Widget Provider] + reads location, recomputes reads location, recomputes + SunTimes locally on loadData() SunTimes in getTimeline() + (every 60s via Timer) (refreshes at next midnight) +``` + +## Build and test + +This is a pure Xcode project. There is no `xcodebuild` wrapper script. + +Run unit tests from Xcode (Product → Test) or via: +``` +xcodebuild test \ + -project Solverv.xcodeproj \ + -scheme Solverv \ + -destination 'platform=iOS Simulator,name=iPhone 16' +``` + +Tests import the main target with `@testable import Solverv`. The widget target is not +separately testable via the current test scheme. + +## Conventions + +- Norwegian event names in `SolsticeData`: Vårjevndøgn, Sommersolverv, Høstjevndøgn, Vintersolverv. +- All astronomical times in `SolsticeData` are UTC; `SunTimes` output is device local time. +- `Season.fromDate(_:)` is calendar-month-based; it does not agree with the astronomical + season encoded in `SolsticeEvent.season`. These are intentionally different. +- Widget timeline policy: `.after(nextMidnight)` — one entry per day. +- `SolvervDef` is the single source of truth for widget view logic; views must not + call `SolsticeData` or `SunTimes` directly. + +## Known issues / landmines + +1. `SolsticeEvent.localDateTime()` applies UTC offset as raw seconds — produces wrong + dates in non-UTC zones with non-whole-hour offsets (e.g., India, Nepal, some AU zones). +2. `AppGroupManager.SunTimes` (the cached struct) is written by the app but the widget + ignores it and recomputes from the cached location. The two caches can diverge. +3. `SolvervDef.init(utcString:)` force-unwraps — preview-only; never call from production. +4. `Item` (SwiftData model) is wired into `SolvervApp.sharedModelContainer` but unused. + Removing it requires a schema migration. +5. `LocationManager` does not handle `.denied` or `.restricted` authorization — the app + shows `—` for sun times without explanation. +6. `SolsticeData` coverage ends at Vintersolverv 2030. After that, all queries return nil. |
