feat: Use Now split button for run-once curves (#251)#34
Conversation
- New BedTempTrendView: three lines (left/right/ambient), time range selector (1H/6H/12H/24H), legend, and summary stats row - Fetch historical bed temp from tRPC environment.getBedTemp endpoint - Add BedTempReading model, getBedTempHistory to protocol + all clients - Fix ISO8601 date parsing to handle fractional seconds - Guard against stale async responses on rapid range switching - Remove old WS-only tempTrendCard (replaced by tRPC-backed chart) - Matches web core's BED TEMPERATURE card
Split the "Apply to Schedule" button into two actions: - Apply to Schedule: existing behavior, persists as recurring weekly entries - Use Now: applies curve from now until wake time via runOnce.start API Adds RunOnceSession model, networking methods (start/getActive/cancel) to protocol and all client implementations.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds bed-temperature history viewing and “Run Once” curve lifecycle: models, protocol and client methods, seed-and-live temp buffering, UI to start/monitor/cancel run-once sessions, AI curve now-variant support, and App Store metadata + minor project wiring. (50 words) Changes
Sequence Diagram(s)mermaid Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (1)
Sleepypod/Networking/SleepypodProtocol.swift (1)
41-41: Consider using a strongly-typedRunOnceSetPointarray instead of[[String: Any]].The
setPoints: [[String: Any]]parameter loses type safety. SinceBetaModels.swiftlikely definesRunOnceSetPoint, using[RunOnceSetPoint](or a similar typed structure) would provide compile-time validation and better documentation of the expected shape.However, if the backend schema is still evolving or the setPoints structure varies, the current approach is a reasonable pragmatic choice.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sleepypod/Networking/SleepypodProtocol.swift` at line 41, The startRunOnce signature uses untyped dictionaries which loses compile-time safety; change the parameter type in func startRunOnce(side: Side, setPoints: [[String: Any]], wakeTime: String) async throws -> RunOnceStartResponse to use a strongly-typed array (e.g., [RunOnceSetPoint]) and update all callers to pass RunOnceSetPoint instances; ensure RunOnceSetPoint in BetaModels.swift conforms to the needed protocols (Codable/Encodable or whatever the networking layer expects) and adjust any JSON encoding/decoding logic inside the implementation of startRunOnce to use the typed model instead of raw dictionaries.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@metadata/app_store_submission.md`:
- Around line 32-70: The fenced code blocks in the metadata content (e.g., the
block starting with "sleepypod — local control for your Eight Sleep Pod", the
tags block "sleep,temperature,eight sleep,pod,...", the numbered steps block
beginning "1. On first launch, tap \"Explore Demo\"", and the release notes
block "Initial release — local control for your Eight Sleep Pod.") are untyped
and trigger MD040; update each triple-backtick fence to include a language
identifier (use text) so they read ```text ... ``` to satisfy markdownlint.
- Around line 134-145: The privacy table currently contradicts itself: the
"Collected? = Yes — on-device only" cell for "Health & Fitness (heart rate,
sleep)" is inconsistent with the later narrative "Data linked to user: No — all
data stays on the local device/pod" and "Data used for tracking: No"; reconcile
these by choosing the exact App Store Connect options and matching wording
across both table and narrative — update the "Collected?" column and/or
narrative to use Apple’s precise labels (e.g., mark Health & Fitness as "No" if
truly never collected/retained off‑device, or keep "Yes" and explicitly state
"Collected but not transmitted/off‑device" and ensure "Linked to User?" and
"Used for Tracking?" reflect that choice) and apply the same change for each row
(Health & Fitness, Usage Data, Diagnostics, Location, Contacts, Identifiers) so
the table cells and the bullet points are fully aligned.
In `@Sleepypod/Views/Schedule/SmartCurveView.swift`:
- Around line 225-244: The "Use Now" CTA must be gated by an explicit run-once
capability instead of just isRunOnce; add a boolean (e.g.,
backendSupportsRunOnce) derived from the backend/client capability (query
APIBackend.current or the created client for a run-once feature flag) and change
the Button to only be shown/enabled when both isRunOnce and
backendSupportsRunOnce are true; ensure the action useNow() and the UI states
(isSaving, showSuccess) are only reachable when that capability is present and
otherwise hide or disable the button so users on the legacy backend
(Sleepypod/Networking/APIBackend.swift / APIBackend.current.createClient())
cannot attempt the unsupported action.
- Around line 795-821: The "Use Now" action currently regenerates a curve via
SleepCurve.generate (using roundedNow, intensity, minTemp, maxTemp) which can
silently replace an on-screen custom curve (curve/customCurvePoints) that
applyToSchedule() expects; either change the behavior so the displayed curve's
datetime is shifted to roundedNow before converting to set points (i.e., update
curve/customCurvePoints' bedtime to roundedNow and reuse that curve when
building setPoints), or disable the "Use Now" action when a custom curve is
active (check customCurvePoints/isCustomCurve flag) and prevent
SleepCurve.generate from running; locate the logic around roundedNow,
SleepCurve.generate(...), curve/customCurvePoints, and applyToSchedule() to
implement one of these fixes.
- Around line 822-849: The success UI is shown even when startRunOnce fails or
only one side succeeds; fix by moving the success/state updates (showSuccess,
Haptics, isRunOnce=false) inside the do block after all startRunOnce calls
succeed, capture the session id(s) returned from
APIBackend.current.createClient().startRunOnce(...) for the first side, and if
starting the second side (when scheduleManager.selectedSide == .both) fails,
call api.cancelRunOnce(sessionId: firstSessionId) to unwind the started session;
ensure the catch block sets isSaving = false and performs any necessary
cleanup/logging so partial failures do not leave a running session or show
"Started!".
In `@Sleepypod/Views/Sensor/BedTempTrendView.swift`:
- Around line 211-223: The code hardcodes "F" when calling getBedTempHistory;
instead read the user-selected temperature unit from the app's settings (use the
same API as other views, e.g. SettingsManager or TemperatureConversion used in
SmartCurveView) and pass that unit string into getBedTempHistory; also ensure
TempPoint construction uses the same conversion/formatter (or accepts values
already in the requested unit) so left/right/ambient/humidity values match the
requested unit. Locate the call to
APIBackend.current.createClient().getBedTempHistory and replace the literal "F"
with the settings-derived unit, and mirror the conversion logic/pattern used in
Sleepypod/Views/Schedule/SmartCurveView (TemperatureConversion/SettingsManager)
when creating TempPoint.
- Around line 226-228: In the catch block in BedTempTrendView where the
active-range request currently swallows errors, clear the stale chart state by
resetting the stored series (set points = []), and update any active-range UI
state (e.g., set a “stale”/loading flag or clear the active range variable) so
the view doesn’t continue rendering previous data; also ensure the UI is updated
on the main thread after modifying points.
---
Nitpick comments:
In `@Sleepypod/Networking/SleepypodProtocol.swift`:
- Line 41: The startRunOnce signature uses untyped dictionaries which loses
compile-time safety; change the parameter type in func startRunOnce(side: Side,
setPoints: [[String: Any]], wakeTime: String) async throws ->
RunOnceStartResponse to use a strongly-typed array (e.g., [RunOnceSetPoint]) and
update all callers to pass RunOnceSetPoint instances; ensure RunOnceSetPoint in
BetaModels.swift conforms to the needed protocols (Codable/Encodable or whatever
the networking layer expects) and adjust any JSON encoding/decoding logic inside
the implementation of startRunOnce to use the typed model instead of raw
dictionaries.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a2b697c8-9d4f-4a73-ad59-784a9a56afe0
📒 Files selected for processing (12)
Sleepypod.xcodeproj/project.pbxprojSleepypod/Models/BetaModels.swiftSleepypod/Networking/FreeSleepClient.swiftSleepypod/Networking/MockClient.swiftSleepypod/Networking/SleepypodCoreClient.swiftSleepypod/Networking/SleepypodProtocol.swiftSleepypod/Services/SensorStreamService.swiftSleepypod/Views/Schedule/SmartCurveView.swiftSleepypod/Views/Sensor/BedSensorScreen.swiftSleepypod/Views/Sensor/BedTempTrendView.swiftSleepypodTests/MockAPIClient.swiftmetadata/app_store_submission.md
| ``` | ||
| sleepypod — local control for your Eight Sleep Pod | ||
|
|
||
| Take control of your sleep environment without the cloud. sleepypod connects directly to your Eight Sleep Pod (3, 4, or 5) on your home network, giving you fast, private, and reliable temperature control. | ||
|
|
||
| TEMPERATURE CONTROL | ||
| - Adjust bed temperature with a precision dial | ||
| - Set independent temperatures for each side | ||
| - Link both sides for synchronized control | ||
|
|
||
| SMART SCHEDULING | ||
| - Create temperature curves for the perfect night | ||
| - Design custom curves with AI assistance | ||
| - Save and reuse your favorite sleep profiles | ||
|
|
||
| ON-DEVICE SLEEP ANALYSIS | ||
| - Sleep stage classification performed on-device using validated heuristics | ||
| - Heart rate and breathing rate extracted from raw piezo signals | ||
| - Sleep quality scoring with circadian-aware algorithms | ||
| - Track weekly trends and movement patterns | ||
|
|
||
| LIVE SENSOR DATA | ||
| - Real-time piezo waveform with DSP-based vitals extraction | ||
| - Bed temperature matrix with zone-level detail | ||
| - Capacitive presence detection with variance analysis | ||
| - Data pipeline visualization showing live sensor data flow | ||
|
|
||
| HAPTIC ALARMS | ||
| - Configure vibration wake-up alarms | ||
| - Choose from preset patterns or create custom ones | ||
| - Set per-side alarm schedules | ||
|
|
||
| PRIVACY FIRST | ||
| - Data is processed and stored locally on your pod — no cloud account required | ||
| - No telemetry or tracking | ||
| - Your sleep data belongs to you | ||
|
|
||
| Requires an Eight Sleep Pod (3, 4, or 5) with sleepypod-core installed and running on your home network. Use "Explore Demo" on first launch to preview the full app experience without hardware. | ||
| ``` |
There was a problem hiding this comment.
Add language identifiers to fenced blocks to clear MD040 warnings.
These code fences are untyped and trigger markdownlint (MD040). Add explicit languages (for example text) to keep docs CI clean.
Suggested patch
-```
+```text
sleepypod — local control for your Eight Sleep Pod
...
Requires an Eight Sleep Pod (3, 4, or 5) with sleepypod-core installed and running on your home network. Use "Explore Demo" on first launch to preview the full app experience without hardware.@@
- +text
sleep,temperature,eight sleep,pod,mattress,sleep tracking,heart rate,HRV,smart bed,local control
@@
-```
+```text
1. On first launch, tap "Explore Demo" to use the app with simulated data
...
To exit demo: Settings gear (top-right) → "Connect to Real Pod"
@@
- +text
Initial release — local control for your Eight Sleep Pod.
...
• Demo mode for exploring without hardware
Also applies to: 74-76, 105-120, 149-158
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 32-32: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@metadata/app_store_submission.md` around lines 32 - 70, The fenced code
blocks in the metadata content (e.g., the block starting with "sleepypod — local
control for your Eight Sleep Pod", the tags block "sleep,temperature,eight
sleep,pod,...", the numbered steps block beginning "1. On first launch, tap
\"Explore Demo\"", and the release notes block "Initial release — local control
for your Eight Sleep Pod.") are untyped and trigger MD040; update each
triple-backtick fence to include a language identifier (use text) so they read
```text ... ``` to satisfy markdownlint.
| | Data Type | Collected? | Linked to User? | Used for Tracking? | | ||
| |-----------|-----------|-----------------|-------------------| | ||
| | Health & Fitness (heart rate, sleep) | Yes — on-device only | No | No | | ||
| | Usage Data | No | — | — | | ||
| | Diagnostics | No | — | — | | ||
| | Location | No | — | — | | ||
| | Contacts | No | — | — | | ||
| | Identifiers | No | — | — | | ||
|
|
||
| - **Data collection purpose**: App Functionality (sleep tracking, temperature control) | ||
| - **Data linked to user**: No — all data stays on the local device/pod | ||
| - **Data used for tracking**: No |
There was a problem hiding this comment.
Reconcile privacy-label wording before App Store submission.
Collected? = Yes — on-device only conflicts with the later claim that data stays local and is not linked/tracked. If this table is used directly for App Store Connect answers, it can create a privacy/compliance mismatch during review. Please align this section with the exact App Privacy definitions you intend to submit.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@metadata/app_store_submission.md` around lines 134 - 145, The privacy table
currently contradicts itself: the "Collected? = Yes — on-device only" cell for
"Health & Fitness (heart rate, sleep)" is inconsistent with the later narrative
"Data linked to user: No — all data stays on the local device/pod" and "Data
used for tracking: No"; reconcile these by choosing the exact App Store Connect
options and matching wording across both table and narrative — update the
"Collected?" column and/or narrative to use Apple’s precise labels (e.g., mark
Health & Fitness as "No" if truly never collected/retained off‑device, or keep
"Yes" and explicitly state "Collected but not transmitted/off‑device" and ensure
"Linked to User?" and "Used for Tracking?" reflect that choice) and apply the
same change for each row (Health & Fitness, Usage Data, Diagnostics, Location,
Contacts, Identifiers) so the table cells and the bullet points are fully
aligned.
| Task { | ||
| do { | ||
| let api = APIBackend.current.createClient() | ||
| let _ = try await api.startRunOnce( | ||
| side: side, | ||
| setPoints: setPoints, | ||
| wakeTime: wakeTimeStr | ||
| ) | ||
|
|
||
| // If both sides, start the other side too | ||
| if scheduleManager.selectedSide == .both { | ||
| let otherSide: Side = side == .left ? .right : .left | ||
| let _ = try await api.startRunOnce( | ||
| side: otherSide, | ||
| setPoints: setPoints, | ||
| wakeTime: wakeTimeStr | ||
| ) | ||
| } | ||
| } catch { | ||
| Log.general.error("Failed to start run-once: \(error)") | ||
| } | ||
|
|
||
| isSaving = false | ||
| withAnimation { showSuccess = true } | ||
| Haptics.heavy() | ||
| try? await Task.sleep(for: .seconds(2)) | ||
| withAnimation { showSuccess = false } | ||
| isRunOnce = false |
There was a problem hiding this comment.
Only show “Started!” after every run-once call succeeds.
The success UI runs after the catch, so a first-call failure—or a second-side failure when .both is selected—still ends in “Started!”. In the partial-failure case, one side can remain running with no cleanup. Move the success state into the do block, and use cancelRunOnce(sessionId:) to unwind the first session if the second start fails.
Proposed fix
Task {
+ let api = APIBackend.current.createClient()
+ var startedSessionId: Int?
do {
- let api = APIBackend.current.createClient()
- let _ = try await api.startRunOnce(
+ let started = try await api.startRunOnce(
side: side,
setPoints: setPoints,
wakeTime: wakeTimeStr
)
+ startedSessionId = started.sessionId
// If both sides, start the other side too
if scheduleManager.selectedSide == .both {
let otherSide: Side = side == .left ? .right : .left
- let _ = try await api.startRunOnce(
+ _ = try await api.startRunOnce(
side: otherSide,
setPoints: setPoints,
wakeTime: wakeTimeStr
)
}
+
+ isSaving = false
+ withAnimation { showSuccess = true }
+ Haptics.heavy()
+ try? await Task.sleep(for: .seconds(2))
+ withAnimation { showSuccess = false }
+ isRunOnce = false
} catch {
+ if let startedSessionId, scheduleManager.selectedSide == .both {
+ try? await api.cancelRunOnce(sessionId: startedSessionId)
+ }
Log.general.error("Failed to start run-once: \(error)")
+ isSaving = false
+ isRunOnce = false
+ return
}
-
- isSaving = false
- withAnimation { showSuccess = true }
- Haptics.heavy()
- try? await Task.sleep(for: .seconds(2))
- withAnimation { showSuccess = false }
- isRunOnce = false
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sleepypod/Views/Schedule/SmartCurveView.swift` around lines 822 - 849, The
success UI is shown even when startRunOnce fails or only one side succeeds; fix
by moving the success/state updates (showSuccess, Haptics, isRunOnce=false)
inside the do block after all startRunOnce calls succeed, capture the session
id(s) returned from APIBackend.current.createClient().startRunOnce(...) for the
first side, and if starting the second side (when scheduleManager.selectedSide
== .both) fails, call api.cancelRunOnce(sessionId: firstSessionId) to unwind the
started session; ensure the catch block sets isSaving = false and performs any
necessary cleanup/logging so partial failures do not leave a running session or
show "Started!".
- Active curve banner on Temp/Schedule/Status screens with chart, NOW marker, and Stop button - Stop Curve powers off the heater - Fade in/out animations on banner - AI curve wizard: Save | Apply | Use Now split bar - Use Now time-shifts curve proportionally (bedtime=now, wake preserved) - Switches to Temp tab after Use Now - Chart: single accent line + phase-colored dots, centered legend - NOW line uses minute-offset interpolation for accurate positioning - X-axis: 4 evenly spaced ticks, rotated labels, bottom padding
The AI prompt now asks for two curves: - "points": reusable template curve (bedtime to wake, as before) - "nowCurve": properly shaped curve starting from current time to wake When the user taps "Use Now", the AI's nowCurve is used if available (properly adapted phases for shorter duration). Falls back to proportional time-shifting if nowCurve is missing. Also: chart line uses single accent color, dots show phase colors, legend centered.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (4)
Sleepypod/Views/Schedule/AICurvePromptView.swift (1)
932-936: Duplicate helper function.
hhmm(_:)duplicatesclockMinutes(_:)fromRunOnceActiveBanner.swift(Lines 118-122). Consider extracting to a shared utility.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sleepypod/Views/Schedule/AICurvePromptView.swift` around lines 932 - 936, The hhmm(_:) helper in AICurvePromptView duplicates clockMinutes(_:) from RunOnceActiveBanner; extract a shared utility (e.g., a new function in a common TimeUtils.swift or an existing shared helper) that implements the time string "HH:mm" -> total minutes logic, replace both hhmm(_:) and clockMinutes(_:) to call that shared function, remove the duplicate implementation from AICurvePromptView and RunOnceActiveBanner, and update any references in those files to use the new shared function name to avoid duplication.Sleepypod/Views/Schedule/ScheduleScreen.swift (2)
60-75: Cancel logic duplicated across screens.The cancel handler (Lines 63-72) duplicates the same pattern found in
TempScreen.stopCurve()(see context snippet 1). Consider extracting to a shared helper or view model method.♻️ Example: Extract to a shared helper
// In a shared location (e.g., RunOnceHelpers.swift or extension on DeviceManager) func cancelRunOnceAndPowerOff(side: Side) async { let api = APIBackend.current.createClient() try? await api.cancelRunOnce(side: side) let powerOff = SideStatusUpdate(isOn: false) var update = DeviceStatusUpdate() if side == .left { update.left = powerOff } else { update.right = powerOff } try? await api.updateDeviceStatus(update) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sleepypod/Views/Schedule/ScheduleScreen.swift` around lines 60 - 75, The cancel handler logic used in RunOnceActiveBanner (inside ScheduleScreen where activeRunOnce is handled) is duplicated with TempScreen.stopCurve(); extract it into a shared async helper (e.g., cancelRunOnceAndPowerOff(side:)) located in a common utility or view-model so both callers reuse it: move the calls that create the client via APIBackend.current.createClient(), call cancelRunOnce(side:), construct SideStatusUpdate(isOn: false) and DeviceStatusUpdate, set the correct left/right field, then call updateDeviceStatus(update); replace the inline Task blocks in ScheduleScreen (RunOnceActiveBanner closure) and TempScreen.stopCurve() to call that shared helper and await it, keeping the withAnimation { activeRunOnce = nil } animation local to ScheduleScreen.
163-169: Potential double-fetch on initial load.Both
.onAppear(Line 165) and.task(id: scheduleManager.selectedSide)(Lines 167-169) will triggerfetchActiveRunOnce()on initial load since.task(id:)also runs immediately. Consider removing the.onAppearcall or using a flag to skip the redundant fetch.💡 Suggested simplification
.onAppear { savedTemplates = CurveTemplate.loadAll() - Task { await fetchActiveRunOnce() } } .task(id: scheduleManager.selectedSide) { await fetchActiveRunOnce() }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sleepypod/Views/Schedule/ScheduleScreen.swift` around lines 163 - 169, The code currently calls fetchActiveRunOnce() both in .onAppear and in .task(id: scheduleManager.selectedSide), causing a redundant initial fetch; remove the Task { await fetchActiveRunOnce() } inside .onAppear and keep savedTemplates = CurveTemplate.loadAll() there, relying on .task(id: scheduleManager.selectedSide) to run initially and on selection changes to perform fetchActiveRunOnce(); alternatively, if you prefer .onAppear to own the initial fetch, remove the .task block instead — but do not call fetchActiveRunOnce() in both places.Sleepypod/Views/Schedule/RunOnceActiveBanner.swift (1)
118-127: Silent failure on malformed time strings.
clockMinutes(_:)returns0for any invalid format (e.g., missing colon, non-numeric parts). This could cause incorrect chart positioning without any error indication. Consider logging a warning for debugging purposes.💡 Optional: Add debug logging for malformed times
private func clockMinutes(_ time: String) -> Int { let parts = time.split(separator: ":") - guard parts.count == 2, let h = Int(parts[0]), let m = Int(parts[1]) else { return 0 } + guard parts.count == 2, let h = Int(parts[0]), let m = Int(parts[1]) else { + `#if` DEBUG + print("⚠️ RunOnceActiveBanner: Invalid time format '\(time)'") + `#endif` + return 0 + } return h * 60 + m }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sleepypod/Views/Schedule/RunOnceActiveBanner.swift` around lines 118 - 127, clockMinutes(_:) currently swallows malformed time strings by returning 0; update it to emit a debug/warning log when the guard fails (include the raw time string and context like "RunOnceActiveBanner" or the affected function name) before returning 0 so malformed inputs are visible during debugging, and keep minuteOffset(_:) behavior unchanged to preserve downstream behavior. Ensure the logging uses the app's existing logging facility (not just print) so logs are consistent with other diagnostics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Sleepypod/Views/Schedule/AICurvePromptView.swift`:
- Around line 880-930: The success UI (withAnimation { isRunOnce = true } and
NotificationCenter.post) is outside the do/catch in useNow(), so failures
(including a failing second startRunOnce when selectedSide == .both) still show
success and don't roll back the first side; move the success handling so it only
runs after all API calls succeed: perform the first startRunOnce, then if
scheduleManager.selectedSide == .both perform the second, and only after both
awaits succeed set isRunOnce, call Haptics.success(), sleep, and post
.switchToTempTab; if the second call fails attempt to rollback the
already-started side by calling the appropriate API stop/cancel method (e.g.,
the client method that cancels a run-once) inside the catch before rethrowing or
logging so partial starts are undone and the success UI is never shown.
In `@Sleepypod/Views/Schedule/RunOnceActiveBanner.swift`:
- Around line 79-100: The Stop button sets isCancelling = true but never resets
it if onCancel returns without dismissing the banner, causing a stuck disabled
UI; update the RunOnceActiveBanner's onCancel signature to accept a completion
callback (e.g., onCancel: (`@escaping` () -> Void) -> Void) and change the Button
action (in the Button/HStack block) to set isCancelling = true, call onCancel {
isCancelling = false } so the caller can signal completion and the UI will
re-enable the button; alternatively, if changing the API is undesired, add a
fallback reset by scheduling a short DispatchQueue.main.asyncAfter timeout to
set isCancelling = false after a few seconds. Ensure you update all call sites
(e.g., TempScreen and ScheduleScreen usages) to call the new onCancel completion
when their cancellation work finishes.
- Around line 140-145: nowMinuteOffset can exceed the chart's visible domain and
place the "Now" RuleMark off-screen; update the computed property
nowMinuteOffset to clamp its result into the visible range [0, totalMinuteSpan]
(use anchorMinutes and totalMinuteSpan to compute the raw offset as before and
then apply min/max or a clamp) so the "Now" marker is always inside the chart
bounds referenced by totalMinuteSpan and used by the RuleMark rendering.
---
Nitpick comments:
In `@Sleepypod/Views/Schedule/AICurvePromptView.swift`:
- Around line 932-936: The hhmm(_:) helper in AICurvePromptView duplicates
clockMinutes(_:) from RunOnceActiveBanner; extract a shared utility (e.g., a new
function in a common TimeUtils.swift or an existing shared helper) that
implements the time string "HH:mm" -> total minutes logic, replace both hhmm(_:)
and clockMinutes(_:) to call that shared function, remove the duplicate
implementation from AICurvePromptView and RunOnceActiveBanner, and update any
references in those files to use the new shared function name to avoid
duplication.
In `@Sleepypod/Views/Schedule/RunOnceActiveBanner.swift`:
- Around line 118-127: clockMinutes(_:) currently swallows malformed time
strings by returning 0; update it to emit a debug/warning log when the guard
fails (include the raw time string and context like "RunOnceActiveBanner" or the
affected function name) before returning 0 so malformed inputs are visible
during debugging, and keep minuteOffset(_:) behavior unchanged to preserve
downstream behavior. Ensure the logging uses the app's existing logging facility
(not just print) so logs are consistent with other diagnostics.
In `@Sleepypod/Views/Schedule/ScheduleScreen.swift`:
- Around line 60-75: The cancel handler logic used in RunOnceActiveBanner
(inside ScheduleScreen where activeRunOnce is handled) is duplicated with
TempScreen.stopCurve(); extract it into a shared async helper (e.g.,
cancelRunOnceAndPowerOff(side:)) located in a common utility or view-model so
both callers reuse it: move the calls that create the client via
APIBackend.current.createClient(), call cancelRunOnce(side:), construct
SideStatusUpdate(isOn: false) and DeviceStatusUpdate, set the correct left/right
field, then call updateDeviceStatus(update); replace the inline Task blocks in
ScheduleScreen (RunOnceActiveBanner closure) and TempScreen.stopCurve() to call
that shared helper and await it, keeping the withAnimation { activeRunOnce = nil
} animation local to ScheduleScreen.
- Around line 163-169: The code currently calls fetchActiveRunOnce() both in
.onAppear and in .task(id: scheduleManager.selectedSide), causing a redundant
initial fetch; remove the Task { await fetchActiveRunOnce() } inside .onAppear
and keep savedTemplates = CurveTemplate.loadAll() there, relying on .task(id:
scheduleManager.selectedSide) to run initially and on selection changes to
perform fetchActiveRunOnce(); alternatively, if you prefer .onAppear to own the
initial fetch, remove the .task block instead — but do not call
fetchActiveRunOnce() in both places.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a81db560-c6aa-4a56-93b4-a09e5e4d7b19
📒 Files selected for processing (8)
Sleepypod.xcodeproj/project.pbxprojSleepypod/SleepypodApp.swiftSleepypod/Views/Schedule/AICurvePromptView.swiftSleepypod/Views/Schedule/RunOnceActiveBanner.swiftSleepypod/Views/Schedule/ScheduleScreen.swiftSleepypod/Views/Schedule/SmartCurveView.swiftSleepypod/Views/Status/StatusScreen.swiftSleepypod/Views/Temp/TempScreen.swift
✅ Files skipped from review due to trivial changes (2)
- Sleepypod/Views/Status/StatusScreen.swift
- Sleepypod.xcodeproj/project.pbxproj
| private func useNow() { | ||
| guard let result = parsedResult else { return } | ||
| let side = scheduleManager.selectedSide.primarySide | ||
|
|
||
| // Shift curve times: remap so bedtime=now, wake stays the same | ||
| let now = Date() | ||
| let cal = Calendar.current | ||
| let nowMins = cal.component(.hour, from: now) * 60 + cal.component(.minute, from: now) | ||
| let roundedNow = (nowMins / 5) * 5 // round to nearest 5 min | ||
| let bedMins = hhmm(result.bedtime) | ||
| let wakeMins = hhmm(result.wake) | ||
| let originalSpan = (wakeMins - bedMins + 1440) % 1440 | ||
| let newSpan = (wakeMins - roundedNow + 1440) % 1440 | ||
|
|
||
| let setPoints: [[String: Any]] = editablePoints.map { p in | ||
| let ptMins = hhmm(p.time) | ||
| let origOffset = (ptMins - bedMins + 1440) % 1440 | ||
| // Scale proportionally into the new span | ||
| let newOffset = originalSpan > 0 ? origOffset * newSpan / originalSpan : origOffset | ||
| let newMins = (roundedNow + newOffset) % 1440 | ||
| let newTime = String(format: "%02d:%02d", newMins / 60, newMins % 60) | ||
| return ["time": newTime, "temperature": p.tempF] | ||
| } | ||
|
|
||
| Task { | ||
| do { | ||
| let api = APIBackend.current.createClient() | ||
| let _ = try await api.startRunOnce( | ||
| side: side, | ||
| setPoints: setPoints, | ||
| wakeTime: result.wake | ||
| ) | ||
|
|
||
| if scheduleManager.selectedSide == .both { | ||
| let other: Side = side == .left ? .right : .left | ||
| let _ = try await api.startRunOnce( | ||
| side: other, | ||
| setPoints: setPoints, | ||
| wakeTime: result.wake | ||
| ) | ||
| } | ||
|
|
||
| withAnimation { isRunOnce = true } | ||
| Haptics.success() | ||
| try? await Task.sleep(for: .seconds(1)) | ||
| NotificationCenter.default.post(name: .switchToTempTab, object: nil) | ||
| } catch { | ||
| Log.general.error("Failed to start run-once from AI curve: \(error)") | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Success UI shown even when API calls fail.
Similar to the past review comment on SmartCurveView (Lines 822-849), the success handling (Lines 922-925) is outside the do block, so failures still show "Started!" and post the tab switch notification. Additionally, if selectedSide == .both and the second call fails, the first side remains running with no rollback.
🐛 Proposed fix: Move success handling inside do block and handle partial failures
Task {
+ let api = APIBackend.current.createClient()
do {
- let api = APIBackend.current.createClient()
let _ = try await api.startRunOnce(
side: side,
setPoints: setPoints,
wakeTime: result.wake
)
if scheduleManager.selectedSide == .both {
let other: Side = side == .left ? .right : .left
- let _ = try await api.startRunOnce(
+ do {
+ let _ = try await api.startRunOnce(
+ side: other,
+ setPoints: setPoints,
+ wakeTime: result.wake
+ )
+ } catch {
+ // Rollback first side on partial failure
+ try? await api.cancelRunOnce(side: side)
+ throw error
+ }
- side: other,
- setPoints: setPoints,
- wakeTime: result.wake
- )
}
- } catch {
- Log.general.error("Failed to start run-once from AI curve: \(error)")
- }
-
- withAnimation { isRunOnce = true }
- Haptics.success()
- try? await Task.sleep(for: .seconds(1))
- NotificationCenter.default.post(name: .switchToTempTab, object: nil)
+ withAnimation { isRunOnce = true }
+ Haptics.success()
+ try? await Task.sleep(for: .seconds(1))
+ NotificationCenter.default.post(name: .switchToTempTab, object: nil)
+ } catch {
+ Log.general.error("Failed to start run-once from AI curve: \(error)")
+ }
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sleepypod/Views/Schedule/AICurvePromptView.swift` around lines 880 - 930, The
success UI (withAnimation { isRunOnce = true } and NotificationCenter.post) is
outside the do/catch in useNow(), so failures (including a failing second
startRunOnce when selectedSide == .both) still show success and don't roll back
the first side; move the success handling so it only runs after all API calls
succeed: perform the first startRunOnce, then if scheduleManager.selectedSide ==
.both perform the second, and only after both awaits succeed set isRunOnce, call
Haptics.success(), sleep, and post .switchToTempTab; if the second call fails
attempt to rollback the already-started side by calling the appropriate API
stop/cancel method (e.g., the client method that cancels a run-once) inside the
catch before rethrowing or logging so partial starts are undone and the success
UI is never shown.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Sleepypod/Services/CurveGenerator.swift`:
- Around line 169-185: When the JSON contains the "nowCurve" key but its
contents are malformed or result in fewer than 3 valid points, do not silently
drop it; instead surface a parse error. In the parsing block that reads
curveJSON["nowCurve"] as? [String: Any] (variables nowRaw, nowPoints, timeRegex,
and nowCurve), validate entries and if any invalid format is detected or
nowPoints.count < 3, return/throw a descriptive parsing error or call the
existing error-reporting path (rather than leaving nowCurve nil), so callers can
detect and handle the provided-but-invalid nowCurve input.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 11106922-7e45-46ed-be3e-529d223bbbe5
📒 Files selected for processing (2)
Sleepypod/Services/CurveGenerator.swiftSleepypod/Views/Schedule/AICurvePromptView.swift
🚧 Files skipped from review as they are similar to previous changes (1)
- Sleepypod/Views/Schedule/AICurvePromptView.swift
| // Parse optional nowCurve | ||
| var nowCurve: [String: Int]? | ||
| if let nowRaw = curveJSON["nowCurve"] as? [String: Any] { | ||
| var nowPoints: [String: Int] = [:] | ||
| for (time, temp) in nowRaw { | ||
| guard time.wholeMatch(of: timeRegex) != nil else { continue } | ||
| if let t = temp as? Int { | ||
| nowPoints[time] = max(55, min(110, t)) | ||
| } else if let t = temp as? Double { | ||
| nowPoints[time] = max(55, min(110, Int(t))) | ||
| } | ||
| } | ||
| if nowPoints.count >= 3 { | ||
| nowCurve = nowPoints | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Don’t silently accept malformed nowCurve when the key is present.
Right now, provided-but-invalid nowCurve data is dropped without surfacing a parse error, which can hide prompt/contract regressions and unexpectedly trigger fallback behavior.
Suggested fix
- if let nowRaw = curveJSON["nowCurve"] as? [String: Any] {
+ if let nowAny = curveJSON["nowCurve"] {
+ guard let nowRaw = nowAny as? [String: Any] else {
+ error = "\"nowCurve\" must be an object when provided."
+ return nil
+ }
var nowPoints: [String: Int] = [:]
for (time, temp) in nowRaw {
- guard time.wholeMatch(of: timeRegex) != nil else { continue }
+ guard time.wholeMatch(of: timeRegex) != nil else { continue }
if let t = temp as? Int {
nowPoints[time] = max(55, min(110, t))
} else if let t = temp as? Double {
nowPoints[time] = max(55, min(110, Int(t)))
}
}
- if nowPoints.count >= 3 {
- nowCurve = nowPoints
- }
+ guard nowPoints.count >= 3 else {
+ error = "\"nowCurve\" must contain at least 3 valid set points when provided."
+ return nil
+ }
+ nowCurve = nowPoints
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sleepypod/Services/CurveGenerator.swift` around lines 169 - 185, When the
JSON contains the "nowCurve" key but its contents are malformed or result in
fewer than 3 valid points, do not silently drop it; instead surface a parse
error. In the parsing block that reads curveJSON["nowCurve"] as? [String: Any]
(variables nowRaw, nowPoints, timeRegex, and nowCurve), validate entries and if
any invalid format is detected or nowPoints.count < 3, return/throw a
descriptive parsing error or call the existing error-reporting path (rather than
leaving nowCurve nil), so callers can detect and handle the provided-but-invalid
nowCurve input.
The Temp page now always shows the active curve — whether it came from a "Use Now" run-once session or today's recurring schedule: 1. Run-once active → show with "ACTIVE CURVE" header + Stop button 2. Today's recurring schedule has set points → show with "TONIGHT'S SCHEDULE" header (no Stop button — edit on Schedule page) 3. Neither → show manual - power + controls Adds ScheduleManager to TempScreen environment, unified ActiveCurve model, and isSchedule flag on RunOnceActiveBanner.
The polling loop and failure handler were calling fetchStatus (HTTP to hardware) while a mutation was in flight, causing hardware contention and temporary disconnects. - Add isSendingMutation flag to block polling during active mutations - Only call fetchStatus on failure when WS isn't providing updates (the event bus already pushes the correct state after mutations) - Trust the WS event bus as the primary state source
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Sleepypod/Views/Temp/TempScreen.swift`:
- Around line 21-37: In stopCurve(), don’t silently ignore errors from
APIBackend.current.createClient() calls (cancelRunOnce and updateDeviceStatus);
instead await them inside a do/catch, handle failures by presenting user
feedback (e.g., call your toast/error handler or set a view-state like
showErrorToast/message on deviceManager), and only clear activeCurve
(withAnimation { activeCurve = nil }) after successful API calls and
fetchStatus; also ensure fetchStatus errors are caught and surfaced so the UI
and backend remain consistent. Use the existing stopCurve,
APIBackend.current.createClient(), cancelRunOnce(side:), updateDeviceStatus(_:)
and deviceManager.fetchStatus() symbols to locate where to add the do/catch,
error-toasting, and conditional activeCurve clearing.
- Around line 39-52: The fetchActiveCurve function currently swallows all errors
by using try? on APIBackend.current.createClient().getActiveRunOnce, so network
failures are treated the same as "no active session"; change this to perform a
proper do/try/catch around getActiveRunOnce (inside fetchActiveCurve) and handle
the cases distinctly: on successful non-nil response construct ActiveCurve as
now, if the API returns nil treat it as "no active run-once" (leave activeCurve
unset), and on thrown/network errors log/record the error (e.g., via
processLogger or a view-model error state) and surface retry or error UI instead
of silently falling back; reference getActiveRunOnce, fetchActiveCurve,
ActiveCurve and APIBackend.current.createClient() when making the change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 92dbdff5-d346-478c-bba9-166d7984ba56
📒 Files selected for processing (2)
Sleepypod/Views/Schedule/RunOnceActiveBanner.swiftSleepypod/Views/Temp/TempScreen.swift
🚧 Files skipped from review as they are similar to previous changes (1)
- Sleepypod/Views/Schedule/RunOnceActiveBanner.swift
| private func stopCurve() { | ||
| guard let curve = activeCurve else { return } | ||
| let side = deviceManager.selectedSide.primarySide | ||
|
|
||
| if curve.source == .runOnce { | ||
| Task { | ||
| let api = APIBackend.current.createClient() | ||
| try? await api.cancelRunOnce(side: side) | ||
| let powerOff = SideStatusUpdate(isOn: false) | ||
| var update = DeviceStatusUpdate() | ||
| if side == .left { update.left = powerOff } else { update.right = powerOff } | ||
| try? await api.updateDeviceStatus(update) | ||
| await deviceManager.fetchStatus() | ||
| withAnimation { activeCurve = nil } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Silent failure on cancel may leave user confused.
Both cancelRunOnce and updateDeviceStatus use try?, silently swallowing errors. If the API call fails, the UI will remain unchanged without any user feedback, yet the backend state remains active.
Consider showing an error toast or keeping the banner visible on failure:
💡 Suggested approach
if curve.source == .runOnce {
Task {
let api = APIBackend.current.createClient()
- try? await api.cancelRunOnce(side: side)
- let powerOff = SideStatusUpdate(isOn: false)
- var update = DeviceStatusUpdate()
- if side == .left { update.left = powerOff } else { update.right = powerOff }
- try? await api.updateDeviceStatus(update)
- await deviceManager.fetchStatus()
- withAnimation { activeCurve = nil }
+ do {
+ try await api.cancelRunOnce(side: side)
+ let powerOff = SideStatusUpdate(isOn: false)
+ var update = DeviceStatusUpdate()
+ if side == .left { update.left = powerOff } else { update.right = powerOff }
+ try await api.updateDeviceStatus(update)
+ await deviceManager.fetchStatus()
+ withAnimation { activeCurve = nil }
+ } catch {
+ // TODO: Show error feedback to user
+ print("Failed to cancel run-once: \(error)")
+ }
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private func stopCurve() { | |
| guard let curve = activeCurve else { return } | |
| let side = deviceManager.selectedSide.primarySide | |
| if curve.source == .runOnce { | |
| Task { | |
| let api = APIBackend.current.createClient() | |
| try? await api.cancelRunOnce(side: side) | |
| let powerOff = SideStatusUpdate(isOn: false) | |
| var update = DeviceStatusUpdate() | |
| if side == .left { update.left = powerOff } else { update.right = powerOff } | |
| try? await api.updateDeviceStatus(update) | |
| await deviceManager.fetchStatus() | |
| withAnimation { activeCurve = nil } | |
| } | |
| } | |
| } | |
| private func stopCurve() { | |
| guard let curve = activeCurve else { return } | |
| let side = deviceManager.selectedSide.primarySide | |
| if curve.source == .runOnce { | |
| Task { | |
| let api = APIBackend.current.createClient() | |
| do { | |
| try await api.cancelRunOnce(side: side) | |
| let powerOff = SideStatusUpdate(isOn: false) | |
| var update = DeviceStatusUpdate() | |
| if side == .left { update.left = powerOff } else { update.right = powerOff } | |
| try await api.updateDeviceStatus(update) | |
| await deviceManager.fetchStatus() | |
| withAnimation { activeCurve = nil } | |
| } catch { | |
| // TODO: Show error feedback to user | |
| print("Failed to cancel run-once: \(error)") | |
| } | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sleepypod/Views/Temp/TempScreen.swift` around lines 21 - 37, In stopCurve(),
don’t silently ignore errors from APIBackend.current.createClient() calls
(cancelRunOnce and updateDeviceStatus); instead await them inside a do/catch,
handle failures by presenting user feedback (e.g., call your toast/error handler
or set a view-state like showErrorToast/message on deviceManager), and only
clear activeCurve (withAnimation { activeCurve = nil }) after successful API
calls and fetchStatus; also ensure fetchStatus errors are caught and surfaced so
the UI and backend remain consistent. Use the existing stopCurve,
APIBackend.current.createClient(), cancelRunOnce(side:), updateDeviceStatus(_:)
and deviceManager.fetchStatus() symbols to locate where to add the do/catch,
error-toasting, and conditional activeCurve clearing.
| private func fetchActiveCurve() async { | ||
| let side = deviceManager.selectedSide.primarySide | ||
|
|
||
| // 1. Check for run-once session (overrides recurring) | ||
| if let session = try? await APIBackend.current.createClient().getActiveRunOnce(side: side) { | ||
| activeCurve = ActiveCurve( | ||
| id: "runonce-\(session.id)", | ||
| source: .runOnce, | ||
| session: session, | ||
| setPoints: session.setPoints, | ||
| wakeTime: session.wakeTime | ||
| ) | ||
| return | ||
| } |
There was a problem hiding this comment.
Network errors silently fall back to schedule display.
Using try? on getActiveRunOnce means a network error causes the UI to show the schedule (or nothing) even if an active run-once session exists on the server. This could mislead users about what's actually controlling their device.
Consider distinguishing between "no active session" (expected nil return) vs "network failure":
💡 Suggested differentiation
-if let session = try? await APIBackend.current.createClient().getActiveRunOnce(side: side) {
+do {
+ if let session = try await APIBackend.current.createClient().getActiveRunOnce(side: side) {
+ activeCurve = ActiveCurve(...)
+ return
+ }
+ // nil means no active session - fall through to schedule
+} catch {
+ // Network error - could show stale state or error indicator
+ print("Failed to fetch run-once status: \(error)")
+}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sleepypod/Views/Temp/TempScreen.swift` around lines 39 - 52, The
fetchActiveCurve function currently swallows all errors by using try? on
APIBackend.current.createClient().getActiveRunOnce, so network failures are
treated the same as "no active session"; change this to perform a proper
do/try/catch around getActiveRunOnce (inside fetchActiveCurve) and handle the
cases distinctly: on successful non-nil response construct ActiveCurve as now,
if the API returns nil treat it as "no active run-once" (leave activeCurve
unset), and on thrown/network errors log/record the error (e.g., via
processLogger or a view-model error state) and surface retry or error UI instead
of silently falling back; reference getActiveRunOnce, fetchActiveCurve,
ActiveCurve and APIBackend.current.createClient() when making the change.
- Hide "Use Now" button when backend is FreeSleep (not supported) - Use displayed curve for "Use Now" (custom or generated, not always regenerated) - Only show "Started!" after all API calls succeed - BedTempTrendView respects user's temperature format (C/F) - Clear stale chart data when range switch fails - isCancelling resets after 5s timeout if banner isn't dismissed - Now marker clamped to chart domain (no off-screen rendering) - Warn on malformed nowCurve (log, don't block main curve)
# Conflicts: # Sleepypod.xcodeproj/project.pbxproj # Sleepypod/Models/BetaModels.swift # Sleepypod/Networking/MockClient.swift # Sleepypod/Views/Sensor/BedTempTrendView.swift
* feat: bed temperature trend chart with historical data (#33) - New BedTempTrendView: three lines (left/right/ambient), time range selector (1H/6H/12H/24H), legend, and summary stats row - Fetch historical bed temp from tRPC environment.getBedTemp endpoint - Add BedTempReading model, getBedTempHistory to protocol + all clients - Fix ISO8601 date parsing to handle fractional seconds - Guard against stale async responses on rapid range switching - Remove old WS-only tempTrendCard (replaced by tRPC-backed chart) - Matches web core's BED TEMPERATURE card * feat: Use Now split button for run-once curves (#251) (#34) * feat: bed temperature trend chart with historical data - New BedTempTrendView: three lines (left/right/ambient), time range selector (1H/6H/12H/24H), legend, and summary stats row - Fetch historical bed temp from tRPC environment.getBedTemp endpoint - Add BedTempReading model, getBedTempHistory to protocol + all clients - Fix ISO8601 date parsing to handle fractional seconds - Guard against stale async responses on rapid range switching - Remove old WS-only tempTrendCard (replaced by tRPC-backed chart) - Matches web core's BED TEMPERATURE card * feat: "Use Now" split button for one-off curve application (#251) Split the "Apply to Schedule" button into two actions: - Apply to Schedule: existing behavior, persists as recurring weekly entries - Use Now: applies curve from now until wake time via runOnce.start API Adds RunOnceSession model, networking methods (start/getActive/cancel) to protocol and all client implementations. * feat: active curve banner, AI curve Use Now, polish - Active curve banner on Temp/Schedule/Status screens with chart, NOW marker, and Stop button - Stop Curve powers off the heater - Fade in/out animations on banner - AI curve wizard: Save | Apply | Use Now split bar - Use Now time-shifts curve proportionally (bedtime=now, wake preserved) - Switches to Temp tab after Use Now - Chart: single accent line + phase-colored dots, centered legend - NOW line uses minute-offset interpolation for accurate positioning - X-axis: 4 evenly spaced ticks, rotated labels, bottom padding * feat: AI prompt returns nowCurve for properly shaped start-now variant The AI prompt now asks for two curves: - "points": reusable template curve (bedtime to wake, as before) - "nowCurve": properly shaped curve starting from current time to wake When the user taps "Use Now", the AI's nowCurve is used if available (properly adapted phases for shorter duration). Falls back to proportional time-shifting if nowCurve is missing. Also: chart line uses single accent color, dots show phase colors, legend centered. * fix: fetch active curve on every Temp tab appearance, not just first load * feat: show tonight's active curve from any source on Temp page The Temp page now always shows the active curve — whether it came from a "Use Now" run-once session or today's recurring schedule: 1. Run-once active → show with "ACTIVE CURVE" header + Stop button 2. Today's recurring schedule has set points → show with "TONIGHT'S SCHEDULE" header (no Stop button — edit on Schedule page) 3. Neither → show manual - power + controls Adds ScheduleManager to TempScreen environment, unified ActiveCurve model, and isSchedule flag on RunOnceActiveBanner. * fix: prevent disconnect/reconnect when adjusting temperature The polling loop and failure handler were calling fetchStatus (HTTP to hardware) while a mutation was in flight, causing hardware contention and temporary disconnects. - Add isSendingMutation flag to block polling during active mutations - Only call fetchStatus on failure when WS isn't providing updates (the event bus already pushes the correct state after mutations) - Trust the WS event bus as the primary state source * fix: eliminate fetchStatus calls during mutations, add 2s cooldown * fix: only show disconnected view when never connected, not on transient failures * fix: sync side selector when Use Now switches to Temp tab * fix: auto-retry mDNS discovery after Local Network permission prompt * fix: add eight-pod.local hostname fallback when mDNS multicast fails * fix: address all PR #34 review feedback - Hide "Use Now" button when backend is FreeSleep (not supported) - Use displayed curve for "Use Now" (custom or generated, not always regenerated) - Only show "Started!" after all API calls succeed - BedTempTrendView respects user's temperature format (C/F) - Clear stale chart data when range switch fails - isCancelling resets after 5s timeout if banner isn't dismissed - Now marker clamped to chart domain (no off-screen rendering) - Warn on malformed nowCurve (log, don't block main curve) * fix: remove processingStatus contract test (endpoint removed in core)
|
🎉 This PR is included in version 1.2.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Summary
Adds "Use Now" button alongside "Apply to Schedule" in SmartCurveView.
runOnce.startAPI to apply from now until wake timeRequires sleepypod/core#251 (run-once backend).
Changes
BetaModels.swift: RunOnceSession, RunOnceStartResponse, RunOnceSetPoint modelsSleepypodProtocol.swift: startRunOnce, getActiveRunOnce, cancelRunOnce methodsSleepypodCoreClient.swift: tRPC implementationsSmartCurveView.swift: split button UI + useNow() functionTest plan
Summary by CodeRabbit
New Features
Improvements