summaryrefslogtreecommitdiffstats
path: root/AGENTS.md
blob: da711d2b79a24789a0a8fc8dd5da8eeccb731ca5 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
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.