summaryrefslogtreecommitdiffstats
path: root/docs/superpowers/specs/2026-03-23-sunrise-sunset-widget-design.md
blob: 1e4ed545318267d028f52479839e079070e2ec6b (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
152
# Sunrise/Sunset Times Widget Feature

**Date:** 2026-03-23
**Status:** Design Approved

## Overview

Add sunrise and sunset time display to both small and medium widgets. Times are calculated locally using the existing NOAA solar position algorithm, formatted as HH:mm, and updated daily at midnight based on user location.

## Requirements

- Display sunrise/sunset times in HH:mm format on widgets
- Calculate times locally using existing `SunTimes` utility
- Use user's current location for calculations
- Update once daily at midnight (aligned with current refresh policy)
- Show nothing if location permission is denied or unavailable
- Apply to both small and medium widget families

## Architecture

### Data Model

Extend `SolvervDef` to include sunrise/sunset times:
```
SolvervDef
├── date: Date
├── bg: String
├── sunriseTime: Date? (optional)
├── sunsetTime: Date? (optional)
├── sunriseFormatted: String (computed)
└── sunsetFormatted: String (computed)
```

**Constructor signature:**
```swift
init(date: Date, sunriseTime: Date? = nil, sunsetTime: Date? = nil) {
    self.date = date
    self.sunriseTime = sunriseTime
    self.sunsetTime = sunsetTime
    self.bg = "smallbg"
}
```

The formatted properties extract HH:mm in device local time.

### Timeline Provider Logic

Update `Provider.getTimeline()`:

1. Get user's cached location from `AppGroupManager` (widget must use App Group storage, not LocationManager)
2. Check if location is recent (less than 24 hours old); if stale/unavailable → create entry with nil sunrise/sunset
3. Initialize local `SunTimes(latitude, longitude, date: currentDate)` calculator
4. Extract sunrise and sunset times (already converted to device local time)
5. Create entry with times: `SolvervEntry(def: SolvervDef(date: currentDate, sunriseTime: sr, sunsetTime: ss))`
6. Return timeline refreshing at next midnight

**Note:** Widget runs in separate process and cannot access main app's LocationManager. Location MUST come from AppGroupManager storage, populated by the main app.

### Data Calculation Flow

```
Location Permission Check
    ↓ (if granted)
Get Cached Coordinates
    ↓
SunTimes Calculator
    ├─ Input: latitude, longitude, date
    ├─ Output: sunrise Date, sunset Date
    └─ Fallback: nil (polar regions, edge cases)
    ↓
Format as HH:mm
    ↓
SolvervDef/Entry
    ↓
Widget Display
```

### Error Handling

| Scenario | Behavior |
|----------|----------|
| Location permission denied | No sunrise/sunset displayed |
| Location unavailable in AppGroupManager | No sunrise/sunset displayed |
| Location older than 24 hours | Treat as unavailable; no sunrise/sunset displayed |
| SunTimes returns nil (polar regions, 24h sun/darkness) | No sunrise/sunset displayed |

### Timezone Handling

- `SunTimes` internally converts to device's local timezone (uses `TimeZone.current`)
- Sunrise/sunset `Date` objects are in device local time
- Formatted display uses device local time (HH:mm)
- **Edge case:** If user changes timezone between midnight and next refresh, sun times remain valid for the previous timezone. They will update correctly at next midnight refresh (when new location may have different timezone). This is acceptable since widget only refreshes daily.

## Implementation Details

### Changes to SolvervDef

Add properties:
- `sunriseTime: Date?` — raw sunrise time in device local timezone
- `sunsetTime: Date?` — raw sunset time in device local timezone

Add computed properties for formatted times:
- `sunriseFormatted: String` — returns "HH:mm" format or empty string if nil
- `sunsetFormatted: String` — returns "HH:mm" format or empty string if nil

Computed properties use device locale and 24-hour format.

### Changes to Provider

Modify `getTimeline()` to:
1. Fetch location from `AppGroupManager.getUserLocation()` — widget runs in separate process and must use App Group storage
2. Check timestamp: if location is older than 24 hours, treat as unavailable
3. If available, instantiate sunrise/sunset calculator: `SunTimes(latitude: lat, longitude: lon, date: currentDate)` from `/Solverv/Utilities/SunTimes.swift`
4. Call `calculator.sunrise()` and `calculator.sunset()` to get Date objects
5. Pass times to `SolvervDef(date: currentDate, sunriseTime: sr, sunsetTime: ss)`
6. Create entry and return timeline refreshing at next midnight (unchanged)

### Widget View Updates

**SmallWidgetView:**
- If sunrise/sunset available: display as "↑ HH:mm ↓ HH:mm" or similar compact format
- If unavailable: display nothing (no placeholder or empty space)

**MediumWidgetView:**
- If sunrise/sunset available: display sunrise and sunset times in HH:mm format (exact layout TBD based on available space)
- If unavailable: display nothing (no placeholder or empty space)

Both views:
- Access times via `entry.def.sunriseFormatted` and `entry.def.sunsetFormatted`
- Only render if both are non-empty strings

## Timeline & Refresh

- **Frequency:** Once per day at midnight
- **Rationale:** Sun times change gradually; daily updates are sufficient
- **Alignment:** Matches existing solstice countdown refresh policy

## Testing

- Test with various locations (tropics, temperate, near poles)
- Test without location permission
- Test at date boundaries (midnight refresh)
- Verify formatted times display correctly
- Verify nil times result in no display

## Future Considerations

- Visualization of sunrise/sunset (charts, gradients)
- Daylight duration calculation and trend
- Multiple locations support
- Custom location override