Skip to content

feat: smart sleep curve — auto temperature schedule#12

Merged
ng merged 22 commits intomainfrom
feat/smart-curve
Mar 16, 2026
Merged

feat: smart sleep curve — auto temperature schedule#12
ng merged 22 commits intomainfrom
feat/smart-curve

Conversation

@ng
Copy link
Copy Markdown
Contributor

@ng ng commented Mar 16, 2026

Summary

Auto-generates a science-backed temperature curve from bedtime + wake time.

User sets two times, picks an intensity (Cool/Balanced/Warm), and the app
generates a 6-phase temperature profile optimized for sleep quality.

Phases

Phase Temp Science
Wind Down +warm Blood vessel dilation (Heller 2012)
Fall Asleep cool Core temp drop initiates sleep (Kräuchi 2007)
Deep Sleep coldest Deepest sleep at lowest core temp
Maintain cool Sustain sleep
Pre-Wake +warm Rising temp triggers natural waking (Czeisler 1999)

UI

  • Schedule page: smart curve card with chart + apply button
  • Existing manual set points under "Advanced Settings" toggle
  • Chart shows offset curve with phase colors and gradient fills

Test plan

  • Curve generates correctly for various bed/wake times
  • Apply saves to schedule API
  • Chart renders with all phases visible

Closes #11

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Interactive Smart Sleep Curve to preview and apply personalized temperature schedules (bedtime, wake time, intensity).
    • Range slider control for selecting min/max temperatures.
    • "Manual Set Points" toggle with animated chevron and haptic feedback.
    • Multi-day schedule selection for choosing multiple weekdays.
    • Import recent sleep times from Health and apply to schedules with success feedback.
  • Privacy / Permissions

    • Health access requested with updated usage description and HealthKit entitlement.

Schedule page:
- Smart curve generator: set bedtime + wake time → auto temperature profile
- Science-backed phases: wind down (+warm), fall asleep (cool), deep sleep
  (coldest), maintain (cool), pre-wake (warm), wake (neutral)
- Three intensity presets: Cool / Balanced / Warm
- Interactive chart with catmullRom curve, phase background colors,
  gradient fills, and offset axis (+/- from base)
- "Apply to Schedule" saves the curve as temperature set points +
  power schedule + alarm time
- Phase legend with colored dots
- Advanced settings collapsed below (existing manual set points)

Models:
- SleepCurve: generates Points from bedtime/wake/intensity
- CoolingIntensity: Cool (-8° deep), Balanced (-6°), Warm (-4°)
- toScheduleTemperatures() converts curve to API format

References: Heller 2012, Kräuchi 2007, Czeisler 1999, Walker 2017

Closes #11

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 16, 2026

Warning

Rate limit exceeded

@ng has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 1 minutes and 0 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 77299441-a7f5-4069-80e3-00c912cfc2eb

📥 Commits

Reviewing files that changed from the base of the PR and between 1c95487 and 5b4069d.

📒 Files selected for processing (2)
  • Sleepypod/Models/SleepCurve.swift
  • Sleepypod/Views/Schedule/SmartCurveView.swift
📝 Walkthrough

Walkthrough

Adds a smart sleep-curve feature: new SleepCurve model, RangeSlider UI, SmartCurveView integrated into ScheduleScreen, HealthKit entitlements and Info.plist key, project wiring for new sources and entitlements, and multi-day selection support in ScheduleManager/DaySelectorView.

Changes

Cohort / File(s) Summary
Project config & entitlements
Sleepypod.xcodeproj/project.pbxproj, project.yml, Sleepypod/Sleepypod.entitlements
Wired new files into Xcode project and target build phases; added CODE_SIGN_ENTITLEMENTS, HealthKit capability, and INFOPLIST_KEY_NSHealthShareUsageDescription.
Sleep curve model
Sleepypod/Models/SleepCurve.swift
New SleepCurve struct producing time-series Points and schedule mapping; includes CoolingIntensity enum and phase-aware offsets.
Smart curve UI
Sleepypod/Views/Schedule/SmartCurveView.swift, Sleepypod/Views/Schedule/ScheduleScreen.swift
New interactive SmartCurveView (chart, bedtime/wake pickers, intensity, HealthKit import, apply-to-schedule) and ScheduleScreen toggling to embed it.
Range slider component
Sleepypod/Views/Schedule/RangeSlider.swift
New RangeSlider SwiftUI component for selecting min/max values with two thumbs and haptic feedback.
Day selection & schedule manager
Sleepypod/Views/Schedule/DaySelectorView.swift, Sleepypod/Services/ScheduleManager.swift
Added selectedDays: Set<DayOfWeek> and updated DaySelectorView to support multi-day toggling while preserving current-day tracking.
Xcode project file additions
Sleepypod.xcodeproj/project.pbxproj
Inserted file references and PBXSources build entries for SleepCurve.swift, RangeSlider.swift, SmartCurveView.swift, and Sleepypod.entitlements; updated groups and build configurations.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant SmartCurveView
    participant SleepCurve
    participant ScheduleManager
    participant APIBackend
    participant HealthKit

    User->>SmartCurveView: Set bedtime, wake time, intensity
    SmartCurveView->>SleepCurve: generate(bedtime, wakeTime, intensity)
    SleepCurve-->>SmartCurveView: [Point] (time, tempOffset, phase)
    SmartCurveView->>SmartCurveView: Render chart & legend
    User->>SmartCurveView: Tap "Apply to Schedule"
    SmartCurveView->>SleepCurve: toScheduleTemperatures([Point])
    SleepCurve-->>SmartCurveView: { "HH:mm": temperature }
    SmartCurveView->>ScheduleManager: Update schedules & selectedDays
    SmartCurveView->>APIBackend: POST updated schedule
    APIBackend-->>SmartCurveView: Success
    SmartCurveView->>User: Show success + haptic
    User->>SmartCurveView: Tap "Import from Health"
    SmartCurveView->>HealthKit: Request sleep data / auth
    HealthKit-->>SmartCurveView: Bedtime/wakeTime
    SmartCurveView->>User: Populate pickers
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I sketched the night in gentle bends,
Warmth to fall and cool that mends.
Pick your bed, pick your wake,
I hop and weave the curve we make.
Apply — sweet dreams on soft fins.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "feat: smart sleep curve — auto temperature schedule" clearly describes the main feature added: automatic temperature schedule generation based on sleep curves.
Linked Issues check ✅ Passed All primary coding objectives from issue #11 are met: SleepCurve struct generates the 6-phase temperature curve, SmartCurveView provides UI for bedtime/wake pickers and interactive chart, ApplyToSchedule persists to API, and Advanced toggle shows manual set points.
Out of Scope Changes check ✅ Passed All changes directly support the smart curve feature: temperature curve generation, UI controls, HealthKit integration for import, entitlements setup, and Schedule page integration are all within scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/smart-curve
📝 Coding Plan
  • Generate coding plan for human review comments

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
Sleepypod/Views/Schedule/SmartCurveView.swift (1)

203-218: Unused variable label in legend tuple.

The label field in the phases tuple is declared but never referenced in the ForEach body.

♻️ Suggested fix
     private var phaseLegend: some View {
-        let phases: [(String, Color, String)] = [
-            ("Wind Down", Theme.warming, "+warm"),
-            ("Fall Asleep", Theme.cooling, "cool"),
-            ("Deep Sleep", Color(hex: "2563eb"), "coldest"),
-            ("Pre-Wake", Theme.amber, "+warm"),
+        let phases: [(name: String, color: Color)] = [
+            ("Wind Down", Theme.warming),
+            ("Fall Asleep", Theme.cooling),
+            ("Deep Sleep", Color(hex: "2563eb")),
+            ("Pre-Wake", Theme.amber),
         ]
         return HStack(spacing: 12) {
-            ForEach(phases, id: \.0) { name, color, label in
+            ForEach(phases, id: \.name) { phase in
                 HStack(spacing: 4) {
-                    Circle().fill(color).frame(width: 6, height: 6)
-                    Text(name)
+                    Circle().fill(phase.color).frame(width: 6, height: 6)
+                    Text(phase.name)
                         .font(.system(size: 9))
                         .foregroundColor(Theme.textSecondary)
                 }
             }
         }
     }
🤖 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 203 - 218, The
tuple element "label" in the phases array is declared but never used; fix by
either removing the third element from the tuple type and each tuple (change
phases to [(String, Color)] and drop the label values) or keep the tuple values
but ignore the unused value in the ForEach closure by destructuring as (name,
color, _) instead of (name, color, label); update the phases declaration and the
ForEach closure in SmartCurveView (phases and the closure parameter list)
accordingly.
Sleepypod/Models/SleepCurve.swift (1)

78-90: Consider documenting key collision behavior.

When multiple points map to the same HH:mm key (e.g., the duplicate points at preWakeStart), the last value overwrites earlier ones. This is currently undocumented and could lead to subtle data loss.

📝 Suggested documentation
     /// Convert curve points to schedule set points (HH:mm → tempF pairs)
+    /// Note: If multiple points share the same HH:mm timestamp, the last one wins.
     static func toScheduleTemperatures(_ points: [Point], baseTempF: Int = 80) -> [String: Int] {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sleepypod/Models/SleepCurve.swift` around lines 78 - 90, Add explicit
documentation to the toScheduleTemperatures(_ points: [Point], baseTempF: Int =
80) function stating that when multiple Point entries yield the same "HH:mm" key
(for example duplicate points at preWakeStart), the later entry in the input
array will overwrite earlier values (last-wins). Mention that keys are generated
via DateFormatter with "HH:mm" and that callers should deduplicate or order
points if they need a different merge behavior. This clarifies the current
behavior without changing implementation.
🤖 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/Models/SleepCurve.swift`:
- Around line 62-70: There are duplicate Point entries at preWakeStart (one with
phase .maintain and one with .preWake); remove the earlier append that adds
Point(time: preWakeStart, tempOffset: offsets.maintain, phase: .maintain) so
only the pre-wake points are added at preWakeStart/wake in SleepCurve (affecting
the points array and the preWakeStart logic using deepSleepEnd, preWakeStart,
wake, offsets, and Point).

In `@Sleepypod/Views/Schedule/SmartCurveView.swift`:
- Around line 285-296: The success animation and haptic run unconditionally
after the do/catch; move the success UI and Haptics.heavy() into the successful
branch so they only run when the API call succeeds. Specifically, after calling
APIBackend.current.createClient() and assigning scheduleManager.schedules = try
await api.updateSchedules(schedules) inside the do block, set isSaving = false,
trigger withAnimation { showSuccess = true }, call Haptics.heavy(), await the
sleep and then hide showSuccess; in the catch branch ensure isSaving = false is
set and do not trigger showSuccess or Haptics (optionally set/trigger an error
state instead). Ensure you update the code paths around
APIBackend.current.createClient(), scheduleManager.schedules, showSuccess,
isSaving, and Haptics.heavy() accordingly.

---

Nitpick comments:
In `@Sleepypod/Models/SleepCurve.swift`:
- Around line 78-90: Add explicit documentation to the toScheduleTemperatures(_
points: [Point], baseTempF: Int = 80) function stating that when multiple Point
entries yield the same "HH:mm" key (for example duplicate points at
preWakeStart), the later entry in the input array will overwrite earlier values
(last-wins). Mention that keys are generated via DateFormatter with "HH:mm" and
that callers should deduplicate or order points if they need a different merge
behavior. This clarifies the current behavior without changing implementation.

In `@Sleepypod/Views/Schedule/SmartCurveView.swift`:
- Around line 203-218: The tuple element "label" in the phases array is declared
but never used; fix by either removing the third element from the tuple type and
each tuple (change phases to [(String, Color)] and drop the label values) or
keep the tuple values but ignore the unused value in the ForEach closure by
destructuring as (name, color, _) instead of (name, color, label); update the
phases declaration and the ForEach closure in SmartCurveView (phases and the
closure parameter list) accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 521fa077-8b1a-4cf4-b057-8af656490b5e

📥 Commits

Reviewing files that changed from the base of the PR and between ce744a2 and 5d2dd7a.

📒 Files selected for processing (4)
  • Sleepypod.xcodeproj/project.pbxproj
  • Sleepypod/Models/SleepCurve.swift
  • Sleepypod/Views/Schedule/ScheduleScreen.swift
  • Sleepypod/Views/Schedule/SmartCurveView.swift

Comment thread Sleepypod/Models/SleepCurve.swift Outdated
Comment on lines +62 to +70
// Maintain: deep sleep end → wake -30min
let preWakeStart = wake.addingTimeInterval(-30 * 60)
if preWakeStart > deepSleepEnd {
points.append(Point(time: preWakeStart, tempOffset: offsets.maintain, phase: .maintain))
}

// Pre-wake: wake -30min → wake (warm up)
points.append(Point(time: preWakeStart, tempOffset: offsets.maintain, phase: .preWake))
points.append(Point(time: wake, tempOffset: offsets.preWake, phase: .preWake))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Duplicate points at preWakeStart time.

When preWakeStart > deepSleepEnd (the typical case), two points are added at exactly preWakeStart: one with phase .maintain (line 65) and another with phase .preWake (line 69). This creates duplicate entries at the same timestamp, which may cause visual artifacts in the chart and ambiguous phase assignments.

🐛 Proposed fix
         // Maintain: deep sleep end → wake -30min
         let preWakeStart = wake.addingTimeInterval(-30 * 60)
         if preWakeStart > deepSleepEnd {
             points.append(Point(time: preWakeStart, tempOffset: offsets.maintain, phase: .maintain))
         }

         // Pre-wake: wake -30min → wake (warm up)
-        points.append(Point(time: preWakeStart, tempOffset: offsets.maintain, phase: .preWake))
+        // Transition point - offset slightly to avoid duplicate timestamp
+        points.append(Point(time: preWakeStart.addingTimeInterval(1), tempOffset: offsets.maintain, phase: .preWake))
         points.append(Point(time: wake, tempOffset: offsets.preWake, phase: .preWake))

Alternatively, consider removing the redundant point entirely since the maintain phase already ends at preWakeStart.

📝 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.

Suggested change
// Maintain: deep sleep end → wake -30min
let preWakeStart = wake.addingTimeInterval(-30 * 60)
if preWakeStart > deepSleepEnd {
points.append(Point(time: preWakeStart, tempOffset: offsets.maintain, phase: .maintain))
}
// Pre-wake: wake -30min → wake (warm up)
points.append(Point(time: preWakeStart, tempOffset: offsets.maintain, phase: .preWake))
points.append(Point(time: wake, tempOffset: offsets.preWake, phase: .preWake))
// Maintain: deep sleep end → wake -30min
let preWakeStart = wake.addingTimeInterval(-30 * 60)
if preWakeStart > deepSleepEnd {
points.append(Point(time: preWakeStart, tempOffset: offsets.maintain, phase: .maintain))
}
// Pre-wake: wake -30min → wake (warm up)
// Transition point - offset slightly to avoid duplicate timestamp
points.append(Point(time: preWakeStart.addingTimeInterval(1), tempOffset: offsets.maintain, phase: .preWake))
points.append(Point(time: wake, tempOffset: offsets.preWake, phase: .preWake))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sleepypod/Models/SleepCurve.swift` around lines 62 - 70, There are duplicate
Point entries at preWakeStart (one with phase .maintain and one with .preWake);
remove the earlier append that adds Point(time: preWakeStart, tempOffset:
offsets.maintain, phase: .maintain) so only the pre-wake points are added at
preWakeStart/wake in SleepCurve (affecting the points array and the preWakeStart
logic using deepSleepEnd, preWakeStart, wake, offsets, and Point).

Comment on lines +285 to +296
do {
let api = APIBackend.current.createClient()
scheduleManager.schedules = try await api.updateSchedules(schedules)
} catch {
Log.general.error("Failed to save smart curve: \(error)")
}

isSaving = false
withAnimation { showSuccess = true }
Haptics.heavy()
try? await Task.sleep(for: .seconds(2))
withAnimation { showSuccess = false }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Success feedback shown even when save fails.

The success animation and haptic feedback execute after the catch block, meaning users see "Applied!" even when the API call fails. The error is only logged, leaving users unaware of the failure.

🐛 Proposed fix
             scheduleManager.schedules = schedules
+            var saveSucceeded = false
             do {
                 let api = APIBackend.current.createClient()
                 scheduleManager.schedules = try await api.updateSchedules(schedules)
+                saveSucceeded = true
             } catch {
                 Log.general.error("Failed to save smart curve: \(error)")
             }

             isSaving = false
-            withAnimation { showSuccess = true }
-            Haptics.heavy()
-            try? await Task.sleep(for: .seconds(2))
-            withAnimation { showSuccess = false }
+            if saveSucceeded {
+                withAnimation { showSuccess = true }
+                Haptics.heavy()
+                try? await Task.sleep(for: .seconds(2))
+                withAnimation { showSuccess = 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 285 - 296, The
success animation and haptic run unconditionally after the do/catch; move the
success UI and Haptics.heavy() into the successful branch so they only run when
the API call succeeds. Specifically, after calling
APIBackend.current.createClient() and assigning scheduleManager.schedules = try
await api.updateSchedules(schedules) inside the do block, set isSaving = false,
trigger withAnimation { showSuccess = true }, call Haptics.heavy(), await the
sleep and then hide showSuccess; in the catch branch ensure isSaving = false is
set and do not trigger showSuccess or Haptics (optionally set/trigger an error
state instead). Ensure you update the code paths around
APIBackend.current.createClient(), scheduleManager.schedules, showSuccess,
isSaving, and Haptics.heavy() accordingly.

ng and others added 4 commits March 15, 2026 20:24
HealthKit (#13):
- "Import from Health" button reads recent sleep sessions
- Extracts bedtime + wake time from HKCategoryValueSleepAnalysis
- Shows "Synced from Health ✓" on success
- Handles permission denied, no data, device not available
- Uses inBed or asleep samples from last 7 days

Chart:
- Y-axis shows user's preferred format (°F, °C, or +/- relative)
- Uses TemperatureConversion.displayTemp for consistency

Schedule:
- Removed redundant controls from Advanced (profile picker, sleep times)
- Advanced now just shows manual phase blocks
- Schedule toggle moved above Advanced

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
HealthKit:
- Added entitlement (com.apple.developer.healthkit) to project.yml
- Added NSHealthShareUsageDescription to Info.plist settings
- Button renamed "Sync from Apple Health" with red heart icon
- Moved to right side of intensity description row (not standalone)
- Shows "Synced" green check after successful import

Curve smoothing:
- ~18 control points (was ~8) for much smoother catmullRom
- Gradual transitions: fall asleep ramps over 45min not 30
- Deep sleep → maintain has 30min transition (not instant)
- Pre-wake warms gradually over 25min with midpoint
- Wake returns to neutral over 20min

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…less

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…elect, smoother curve

HealthKit:
- Now queries future-dated samples first (iOS Sleep Schedule writes
  forward-looking inBed samples for tonight/tomorrow)
- Falls back to past 7 days of sleep analysis data
- Fixed: was only looking at past data, missing the schedule

Curve smoothing:
- All transitions model real water thermal lag (~15-20 min per °F)
- Fall asleep: 90 min gradual ramp (was 30 min)
- Deep→maintain: 45 min transition (was instant)
- Pre-wake: 45 min warm (was 25 min)
- Wake→neutral: 30 min (was 20 min)
- ~25 control points for very smooth catmullRom

Day selector:
- Multi-select: tap multiple days, apply curve to all at once
- Must keep at least one day selected
- Schedule manager tracks selectedDays set

Schedule cleanup:
- Advanced only shows manual phase blocks
- Schedule toggle above Advanced

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sleepypod/Views/Schedule/ScheduleScreen.swift (1)

42-55: ⚠️ Potential issue | 🟡 Minor

Phase blocks are always visible despite "Manual Set Points" toggle.

The showAdvanced state controls the chevron rotation but doesn't actually show/hide the phase blocks. The button suggests these are "advanced" settings that should be collapsible, but the ForEach is rendered unconditionally.

🐛 Proposed fix to conditionally show phase blocks
-                // Phase blocks
-                if scheduleManager.schedules != nil {
+                // Phase blocks (advanced)
+                if showAdvanced, scheduleManager.schedules != nil {
                     VStack(spacing: 12) {
                         ForEach(scheduleManager.phases) { phase in
                             PhaseBlockView(phase: phase)
                         }
                     }
🤖 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 42 - 55, The
phase block list is always rendered even when the "Manual Set Points" toggle is
collapsed; update the view so the VStack/ForEach that builds PhaseBlockView
(ForEach(scheduleManager.phases) { phase in PhaseBlockView(phase: phase) }) is
only shown when the local state showAdvanced is true (in addition to the
existing scheduleManager.schedules/isLoading checks). Concretely, wrap the
existing VStack( spacing: 12 ) { ForEach(...) } branch in a conditional that
checks showAdvanced (preserving the current else-if isLoading and fallback Text
branches), and keep the chevron rotation logic tied to showAdvanced so the
header toggle controls the visibility of the PhaseBlockView list.
♻️ Duplicate comments (1)
Sleepypod/Views/Schedule/SmartCurveView.swift (1)

406-417: ⚠️ Potential issue | 🟠 Major

Success feedback still shown even when save fails.

The success animation and haptic feedback execute unconditionally after the catch block, meaning users see "Applied!" even when the API call fails.

🐛 Proposed fix
             scheduleManager.schedules = schedules
+            var saveSucceeded = false
             do {
                 let api = APIBackend.current.createClient()
                 scheduleManager.schedules = try await api.updateSchedules(schedules)
+                saveSucceeded = true
             } catch {
                 Log.general.error("Failed to save smart curve: \(error)")
             }

             isSaving = false
-            withAnimation { showSuccess = true }
-            Haptics.heavy()
-            try? await Task.sleep(for: .seconds(2))
-            withAnimation { showSuccess = false }
+            if saveSucceeded {
+                withAnimation { showSuccess = true }
+                Haptics.heavy()
+                try? await Task.sleep(for: .seconds(2))
+                withAnimation { showSuccess = 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 406 - 417, The
success animation and haptic are being run regardless of failure; update the
control flow so the success UI/haptic (setting showSuccess = true withAnimation
and Haptics.heavy(), the 2s sleep, and hiding showSuccess) only runs after a
successful await of APIBackend.current.createClient().updateSchedules(...)
inside the do block (where you set scheduleManager.schedules), and ensure
isSaving is still cleared in both success and catch paths (set isSaving = false
in a finally-equivalent location or set it in both the do and catch); reference
the symbols APIBackend.current.createClient(), updateSchedules,
scheduleManager.schedules, isSaving, showSuccess, and Haptics.heavy() to locate
and change the code accordingly.
🧹 Nitpick comments (2)
Sleepypod/Views/Schedule/SmartCurveView.swift (2)

238-255: Unused label variable in phase legend tuple.

The label component of the tuple (e.g., "+warm", "cool") is destructured but never used in the view.

♻️ Proposed cleanup
     private var phaseLegend: some View {
-        let phases: [(String, Color, String)] = [
-            ("Wind Down", Theme.warming, "+warm"),
-            ("Fall Asleep", Theme.cooling, "cool"),
-            ("Deep Sleep", Color(hex: "2563eb"), "coldest"),
-            ("Pre-Wake", Theme.amber, "+warm"),
+        let phases: [(String, Color)] = [
+            ("Wind Down", Theme.warming),
+            ("Fall Asleep", Theme.cooling),
+            ("Deep Sleep", Color(hex: "2563eb")),
+            ("Pre-Wake", Theme.amber),
         ]
         return HStack(spacing: 12) {
-            ForEach(phases, id: \.0) { name, color, label in
+            ForEach(phases, id: \.0) { name, color in
                 HStack(spacing: 4) {
                     Circle().fill(color).frame(width: 6, height: 6)
                     Text(name)
🤖 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 238 - 255, The
tuple element "label" in the phases array is destructured but never used; in the
phaseLegend computed var remove the third tuple element from phases (use
[(String, Color)] instead of [(String, Color, String)]) and update the ForEach
closure to only unpack (name, color), or if the label is intended to be shown,
use the destructured label inside the HStack (e.g., add a Text(label) with
appropriate styling). Update the phases declaration and the ForEach signature
(and any references to label) in phaseLegend to keep the code consistent.

291-297: Potential data race with strict concurrency enabled.

With SWIFT_STRICT_CONCURRENCY: complete enabled, updating @State properties from a non-@MainActor context can trigger data race warnings or runtime issues. The requestAuthorization callback executes on a background queue, and while you dispatch to main for state updates, the closure itself captures self in a non-isolated context.

Consider using the async/await HealthKit API (requestAuthorization(toShare:read:) async variant) within a Task { @mainactor in ... } block, or mark the entire function with @MainActor.

♻️ Suggested approach using async API
`@MainActor`
private func importFromHealth() async {
    guard HKHealthStore.isHealthDataAvailable() else {
        healthError = "Health data not available on this device"
        return
    }
    
    let store = HKHealthStore()
    let sleepType = HKCategoryType(.sleepAnalysis)
    
    do {
        try await store.requestAuthorization(toShare: [], read: [sleepType])
        // ... rest of query logic using async APIs
    } catch {
        healthError = "Health access denied — enable in Settings → Privacy → Health"
    }
}
🤖 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 291 - 297, The
requestAuthorization callback is capturing self on a background thread and then
mutating the `@State` property healthError, which can cause strict-concurrency
data races; to fix, convert the import flow to use the async HealthKit API or
ensure main-actor isolation: mark the surrounding method (e.g.,
importFromHealth) as `@MainActor` or wrap async work with Task { `@MainActor` in ...
}, call the async requestAuthorization(toShare:read:) variant with try/await (or
call DispatchQueue.main only inside a `@MainActor` context), and then assign
healthError directly on the main actor so the closure does not capture
non-isolated self.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@project.yml`:
- Around line 28-33: Remove the unnecessary Clinical Health Records entitlement
by deleting the "health-records" entry from the
com.apple.developer.healthkit.access list in the entitlements (the block under
entitlements -> path: Sleepypod/Sleepypod.entitlements); leave
com.apple.developer.healthkit: true intact since SmartCurveView.swift only uses
HKCategoryType(.sleepAnalysis) and does not require HKClinicalRecord APIs, so do
not add any other clinical entitlements unless you later use HKClinicalRecord or
related Clinical Health Record APIs.

In `@Sleepypod/Models/SleepCurve.swift`:
- Around line 59-76: The phase timeline breaks for very short sleepDuration
values causing overlap between deepSleepEnd/transitionEnd and preWakeStart; fix
by enforcing a minimum effective duration or clamping phase boundaries before
appending points: compute an effectiveSleep = max(sleepDuration,
minDurationInSeconds) or clamp deepSleepEnd/transitionEnd so transitionEnd <=
preWakeStart, and only append Point(...) for deepSleepEnd, transitionEnd,
maintainMid, and preWakeStart when their times are strictly increasing (or
adjust their times proportionally); update the code paths referencing
deepSleepEnd, transitionEnd, preWakeStart, and points to use the
clamped/effective times to ensure phases remain chronological and semantically
correct.

---

Outside diff comments:
In `@Sleepypod/Views/Schedule/ScheduleScreen.swift`:
- Around line 42-55: The phase block list is always rendered even when the
"Manual Set Points" toggle is collapsed; update the view so the VStack/ForEach
that builds PhaseBlockView (ForEach(scheduleManager.phases) { phase in
PhaseBlockView(phase: phase) }) is only shown when the local state showAdvanced
is true (in addition to the existing scheduleManager.schedules/isLoading
checks). Concretely, wrap the existing VStack( spacing: 12 ) { ForEach(...) }
branch in a conditional that checks showAdvanced (preserving the current else-if
isLoading and fallback Text branches), and keep the chevron rotation logic tied
to showAdvanced so the header toggle controls the visibility of the
PhaseBlockView list.

---

Duplicate comments:
In `@Sleepypod/Views/Schedule/SmartCurveView.swift`:
- Around line 406-417: The success animation and haptic are being run regardless
of failure; update the control flow so the success UI/haptic (setting
showSuccess = true withAnimation and Haptics.heavy(), the 2s sleep, and hiding
showSuccess) only runs after a successful await of
APIBackend.current.createClient().updateSchedules(...) inside the do block
(where you set scheduleManager.schedules), and ensure isSaving is still cleared
in both success and catch paths (set isSaving = false in a finally-equivalent
location or set it in both the do and catch); reference the symbols
APIBackend.current.createClient(), updateSchedules, scheduleManager.schedules,
isSaving, showSuccess, and Haptics.heavy() to locate and change the code
accordingly.

---

Nitpick comments:
In `@Sleepypod/Views/Schedule/SmartCurveView.swift`:
- Around line 238-255: The tuple element "label" in the phases array is
destructured but never used; in the phaseLegend computed var remove the third
tuple element from phases (use [(String, Color)] instead of [(String, Color,
String)]) and update the ForEach closure to only unpack (name, color), or if the
label is intended to be shown, use the destructured label inside the HStack
(e.g., add a Text(label) with appropriate styling). Update the phases
declaration and the ForEach signature (and any references to label) in
phaseLegend to keep the code consistent.
- Around line 291-297: The requestAuthorization callback is capturing self on a
background thread and then mutating the `@State` property healthError, which can
cause strict-concurrency data races; to fix, convert the import flow to use the
async HealthKit API or ensure main-actor isolation: mark the surrounding method
(e.g., importFromHealth) as `@MainActor` or wrap async work with Task { `@MainActor`
in ... }, call the async requestAuthorization(toShare:read:) variant with
try/await (or call DispatchQueue.main only inside a `@MainActor` context), and
then assign healthError directly on the main actor so the closure does not
capture non-isolated self.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 92e0c7a0-f435-40ca-92a1-b2ad769e46cc

📥 Commits

Reviewing files that changed from the base of the PR and between 5d2dd7a and aef491c.

📒 Files selected for processing (6)
  • Sleepypod.xcodeproj/project.pbxproj
  • Sleepypod/Models/SleepCurve.swift
  • Sleepypod/Sleepypod.entitlements
  • Sleepypod/Views/Schedule/ScheduleScreen.swift
  • Sleepypod/Views/Schedule/SmartCurveView.swift
  • project.yml
✅ Files skipped from review due to trivial changes (1)
  • Sleepypod/Sleepypod.entitlements

Comment thread project.yml
Comment on lines +28 to +33
entitlements:
path: Sleepypod/Sleepypod.entitlements
properties:
com.apple.developer.healthkit: true
com.apple.developer.healthkit.access:
- health-records
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

iOS HealthKit health-records vs standard sleep analysis entitlements

💡 Result:

Standard HealthKit sleep analysis (e.g., HKCategoryTypeIdentifier.sleepAnalysis)

  • Entitlement / capability: Just enable the HealthKit capability, which adds the com.apple.developer.healthkit.access entitlement to the app. (Some related HealthKit entitlements, like background delivery, are separate toggles, but sleep itself doesn’t require a special entitlement beyond HealthKit.) [1]
  • Info.plist: NSHealthShareUsageDescription (read) and NSHealthUpdateUsageDescription (write, only if you write) [2].
  • Runtime permission: Use HKHealthStore.requestAuthorization(...) for .sleepAnalysis like any other HealthKit type [2].

HealthKit “Health Records” / Clinical Records (e.g., HKClinicalRecord, HKClinicalTypeIdentifier.*)

  • Extra capability beyond basic HealthKit: You must enable the Health Records / Clinical Health Records capability for the app (App Store Connect / developer portal); Apple will reject apps that reference the Clinical Health Records API without an appropriate primary feature/use case [3].
  • Info.plist: Add NSHealthClinicalHealthRecordsShareUsageDescription (purpose string specifically for clinical records) [4].
  • Authorization characteristics: Clinical record samples are read-only (you can’t request “share/write” authorization for clinical record types) [5], and you query/read them via HKClinicalRecord [6].
  • Optional “required types” declaration: If your app requires certain clinical record types to function, list them under NSHealthRequiredReadAuthorizationTypeIdentifiers [2].

Verifiable Health Records (SMART Health Cards, etc.)

  • Not the same as general Health Records: Accessing verifiable health records uses a dedicated flow and (per Apple’s WWDC guidance) requires requesting a specific HealthKit access entitlement for Verifiable Health Records [7].

Sources:
[1] Apple Developer Documentation — com.apple.developer.healthkit.background-delivery (notes key com.apple.developer.healthkit.access) https://developer.apple.com/documentation/bundleresources/entitlements/com.apple.developer.healthkit.background-delivery
[2] Apple Developer Documentation — Authorizing access to health data https://developer.apple.com/documentation/healthkit/authorizing-access-to-health-data
[3] Stack Overflow (quoting App Review guidance) — Health Records capability / Clinical Health Records API rejection context https://stackoverflow.com/questions/63009308/cant-seem-to-disable-the-health-records-capability-within-healthkit-in-either-x
[4] Apple Info.plist Key Reference — NSHealthClinicalHealthRecordsShareUsageDescription https://developer-mdn.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html
[5] Apple Developer Documentation — HKClinicalTypeIdentifier (clinical records are read-only) https://developer.apple.com/documentation/healthkit/hkclinicaltypeidentifier
[6] Apple Developer Documentation — HKClinicalRecord https://developer.apple.com/documentation/healthkit/hkclinicalrecord
[7] Apple Developer (WWDC21) — Explore Verifiable Health Records (mentions special entitlement) https://developer.apple.com/videos/play/wwdc2021/10089/


🏁 Script executed:

# Find and examine the Sleepypod.entitlements file
fd -t f "Sleepypod.entitlements" -x cat {}

Repository: sleepypod/ios

Length of output: 397


🏁 Script executed:

# Find SmartCurveView.swift and check HealthKit usage
fd -t f "SmartCurveView.swift" -x cat {}

Repository: sleepypod/ios

Length of output: 16275


🏁 Script executed:

# Search for any clinical health record references in the codebase
rg "HKClinical|NSHealthClinicalHealthRecordsShareUsageDescription" --type swift --type plist

Repository: sleepypod/ios

Length of output: 86


🏁 Script executed:

# Check project.yml for the full entitlements context
fd -t f "project.yml" -x head -50 {}

Repository: sleepypod/ios

Length of output: 1665


🌐 Web query:

com.apple.developer.healthkit.access entitlement array valid values iOS

💡 Result:

For iOS, the entitlement com.apple.developer.healthkit.access is an array of strings used to declare HealthKit data types that require additional permission beyond the base com.apple.developer.healthkit entitlement. [1]

Valid array values

  • health-records — enables Clinical Health Records access (the “Health Records”/clinical records capability). This is the value that appears in provisioning profiles and entitlements when Clinical Health Records is enabled. [2][3]

Apple’s public docs/tools surface health-records as the (currently) known/used value for this entitlement. [1][2][3]

Sources:
[1] Apple Developer Documentation (Entitlements reference / HealthKit entitlements index via background-delivery page) (developer.apple.com)
[2] Stack Overflow (provisioning profile shows com.apple.developer.healthkit.access = health-records) (stackoverflow.com)
[3] NativeScript plugin docs showing the entitlement value health-records (market.nativescript.org)

Citations:


🌐 Web query:

Apple HealthKit entitlement access property list values documentation

💡 Result:

For HealthKit, the entitlement values you put in your app’s .entitlements (plist) are:

  • com.apple.developer.healthkit: Boolean (true) — enables the app to request HealthKit authorization. (developer.apple.com)
  • com.apple.developer.healthkit.access: Array of strings — used only for HealthKit features that require additional permission beyond basic HealthKit. (developer.apple.com)
    • A commonly-used (and App Review–relevant) value is health-records (Clinical Health Records). (stackoverflow.com)
  • (Related) com.apple.developer.healthkit.background-delivery: Boolean (true) — allows background delivery for HKObserverQuery. (developer.apple.com)

Minimal example:

<!-- YourApp.entitlements -->
<plist version="1.0">
<dict>
  <key>com.apple.developer.healthkit</key>
  <true/>

  <!-- Only if you actually use Clinical Health Records APIs/features -->
  <key>com.apple.developer.healthkit.access</key>
  <array>
    <string>health-records</string>
  </array>
</dict>
</plist>

Also ensure your Info.plist includes the required privacy strings, e.g.:

Citations:


Remove unnecessary health-records entitlement; it's not used by this app.

The entitlements declare com.apple.developer.healthkit.access: [health-records], which enables Clinical Health Records access. However, SmartCurveView.swift uses only HKCategoryType(.sleepAnalysis), which is standard HealthKit requiring no special entitlements beyond com.apple.developer.healthkit: true.

The health-records value should be removed unless you later add Clinical Health Record APIs (e.g., HKClinicalRecord). Declaring this capability without a justified use case may cause App Review rejection or scrutiny.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@project.yml` around lines 28 - 33, Remove the unnecessary Clinical Health
Records entitlement by deleting the "health-records" entry from the
com.apple.developer.healthkit.access list in the entitlements (the block under
entitlements -> path: Sleepypod/Sleepypod.entitlements); leave
com.apple.developer.healthkit: true intact since SmartCurveView.swift only uses
HKCategoryType(.sleepAnalysis) and does not require HKClinicalRecord APIs, so do
not add any other clinical entitlements unless you later use HKClinicalRecord or
related Clinical Health Record APIs.

Comment thread Sleepypod/Models/SleepCurve.swift Outdated
ng and others added 5 commits March 15, 2026 20:52
Temp range:
- Two sliders: Coolest (55-78°F) and Warmest (78-110°F)
- Curve offsets scale proportionally to the user's range
- Chart Y-axis adapts to the selected range
- Shows "68° – 86°F" summary

Health sync:
- Removed "Sync from Apple Health" button — Apple does not expose
  the Sleep Schedule through any public API (confirmed via Apple
  Developer Forums, HealthKit docs, WWDC 2025). sleepAnalysis only
  contains historical data, not the configured schedule.
- Filed as known limitation in iOS #13

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Range slider:
- Single track, two draggable thumbs (blue=cool, orange=warm)
- Gradient fill between thumbs (blue → gray → orange)
- Haptic feedback on drag
- Thumbs can't cross each other
- Labels show both temps in user's preferred format

Chart:
- Fixed Y-axis (-20 to +20) — doesn't rescale with slider changes
- Curve scales to the user's range but chart stays stable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Slider moved below chart so dragging doesn't cover it.
Adaptive range: centers on current midpoint ±15°F. So if you're
at 80-85°F, the slider shows 67-97°F instead of 55-110°F, giving
much more resolution for fine adjustments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Temp range:
- Replaced dual slider with horizontal drag lines on the chart
- Blue dashed line = min (drag up/down), orange = max
- Labels on right edge show current temp
- "Drag lines to adjust" hint below

Chart fixes:
- Bedtime defaults to tonight 22:00, wake to tomorrow 07:00
  (was defaulting to today which missed pre-midnight hours)
- Removed phase background rectangles (gaps between stages)
- Removed separate RangeSlider component (replaced by chart overlay)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (2)
Sleepypod/Views/Schedule/SmartCurveView.swift (1)

501-513: ⚠️ Potential issue | 🟠 Major

Show “Applied!” only when the API save succeeds.

Failure path (Line 505-507) still falls through to success animation/haptic at Line 510-513, so users get false success feedback.

Proposed fix
             scheduleManager.schedules = schedules
+            var saveSucceeded = false
             do {
                 let api = APIBackend.current.createClient()
                 scheduleManager.schedules = try await api.updateSchedules(schedules)
+                saveSucceeded = true
             } catch {
                 Log.general.error("Failed to save smart curve: \(error)")
             }

             isSaving = false
-            withAnimation { showSuccess = true }
-            Haptics.heavy()
-            try? await Task.sleep(for: .seconds(2))
-            withAnimation { showSuccess = false }
+            if saveSucceeded {
+                withAnimation { showSuccess = true }
+                Haptics.heavy()
+                try? await Task.sleep(for: .seconds(2))
+                withAnimation { showSuccess = 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 501 - 513, The
code currently sets scheduleManager.schedules and then calls the API
(APIBackend.current.createClient()/updateSchedules) but always proceeds to clear
isSaving, show the success animation/haptic (isSaving = false, withAnimation {
showSuccess = true }, Haptics.heavy(), sleep, withAnimation { showSuccess =
false }) even when the API throws; change the flow so that
scheduleManager.schedules is only updated with the API result (assign the result
of try await api.updateSchedules(schedules) to scheduleManager.schedules) and
move the success UI/haptic block (isSaving = false, showSuccess animation,
Haptics.heavy(), sleep, hide success) into the success path after the await, and
ensure the catch block sets isSaving = false and does not trigger showSuccess
(or returns/throws) so failure does not show “Applied!”.
Sleepypod/Models/SleepCurve.swift (1)

84-119: ⚠️ Potential issue | 🟡 Minor

Very short sleep windows can still produce overlapped/semantically broken phase timelines.

For short durations, preWakeStart can move into earlier phases, and sorting hides timeline validity problems instead of preventing them. Add minimum-duration fallback or clamp boundaries to strictly increasing times before appending points.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sleepypod/Models/SleepCurve.swift` around lines 84 - 119, The phase timeline
can produce non-monotonic/overlapping Point.times for short sleeps (variables:
coolRamp, deepTransition, deepStart, deepSleepEnd, maintainStart, preWakeStart,
points), so enforce strictly increasing timestamps before appending: introduce a
small minPhaseGap (e.g. 1–5 minutes) and clamp each computed start time to be at
least previousPoint.time.addingTimeInterval(minPhaseGap); adjust maintainStart
and preWakeStart using max(desired, previous + minGap) and only append Points
when their times are strictly greater than the last appended point.time; remove
reliance on later sorting to hide invalid timelines and ensure phases (coolDown
→ deepSleep → maintain → preWake) always advance monotonically.
🧹 Nitpick comments (1)
Sleepypod/Views/Schedule/SmartCurveView.swift (1)

503-504: Prefer using ScheduleManager’s API boundary instead of creating a client in the view.

Calling APIBackend.current.createClient() directly in SmartCurveView bypasses your injected manager dependency and makes this flow harder to test consistently.

🤖 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 503 - 504, The
view is directly calling APIBackend.current.createClient() and then
api.updateSchedules(schedules), which bypasses the injected ScheduleManager;
instead, add or use a ScheduleManager API boundary method (e.g.,
ScheduleManager.updateSchedules(_:), or a clearly named wrapper like
performRemoteUpdate(schedules:)) that encapsulates creating the API client and
performing api.updateSchedules(schedules), and replace the view call with
scheduleManager.updateSchedules(schedules) so the networking is owned by
ScheduleManager and the view remains testable.
🤖 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/Models/SleepCurve.swift`:
- Around line 49-58: The scale behavior can invert when coolRoom or warmRoom is
negative; clamp both available ranges to non-negative values before using them
in scale(_ offset: Int). Replace the computed coolRoom and warmRoom with clamped
versions (e.g., coolRoomClamped = max(baseTempF - minTempF, 0) and
warmRoomClamped = max(maxTempF - baseTempF, 0)) and use those in the scale
function, and if a clamped range is zero ensure scale returns 0 for that
direction to avoid inverted or division-by-zero effects in SleepCurve.scale(_:).

In `@Sleepypod/Services/ScheduleManager.swift`:
- Line 9: selectedDays was added but write APIs still only mutate selectedDay,
causing inconsistent persistence; update togglePowerSchedule, updateAlarmTime,
updateBedtime, updatePhaseTemperature, and applyProfile to apply their changes
for every day in selectedDays (e.g., iterate selectedDays and call the existing
per-day write/persist logic or extract that per-day write into a helper and
invoke it for each DayOfWeek), ensuring UI state updates reflect the full set
and using the same persistence/notification path so multi-day edits are actually
saved for each day rather than only selectedDay.

In `@Sleepypod/Views/Schedule/DaySelectorView.swift`:
- Around line 12-17: The bug is that scheduleManager.selectedDay is set
unconditionally which can leave selectedDay pointing to a deselected day; in the
DaySelectorView handler that currently mutates scheduleManager.selectedDays and
then sets scheduleManager.selectedDay, change the logic so selectedDay is only
set when a day is being selected, and when a day is being removed (in the branch
that calls scheduleManager.selectedDays.remove(day)) update
scheduleManager.selectedDay to a valid value (e.g., set it to nil or to a
remaining selectedDays.first) to keep scheduleManager.selectedDay synchronized
with scheduleManager.selectedDays.

In `@Sleepypod/Views/Schedule/RangeSlider.swift`:
- Around line 12-17: The drag math for the thumbs uses local coordinates and
unguarded divisions which can produce jumps and NaN/Inf; update the track view
produced inside GeometryReader to set a named coordinate space (e.g.,
.coordinateSpace(name: "track")), switch the thumbs' DragGesture to
DragGesture(coordinateSpace: .named("track")) so value.location.x is
track-relative, and add defensive guards around all divisors used in the mapping
logic (check step != 0, span = range.upperBound - range.lowerBound != 0, and
width = geo.size.width - thumbSize > 0) inside whatever functions/closures
compute the normalized position and step-snapped value (refer to span, range,
thumbSize, and the DragGesture handlers); if any divisor is invalid,
early-return or clamp to a well-defined fallback (current bound or min/max) to
avoid NaN/Inf and discontinuous jumps.

---

Duplicate comments:
In `@Sleepypod/Models/SleepCurve.swift`:
- Around line 84-119: The phase timeline can produce non-monotonic/overlapping
Point.times for short sleeps (variables: coolRamp, deepTransition, deepStart,
deepSleepEnd, maintainStart, preWakeStart, points), so enforce strictly
increasing timestamps before appending: introduce a small minPhaseGap (e.g. 1–5
minutes) and clamp each computed start time to be at least
previousPoint.time.addingTimeInterval(minPhaseGap); adjust maintainStart and
preWakeStart using max(desired, previous + minGap) and only append Points when
their times are strictly greater than the last appended point.time; remove
reliance on later sorting to hide invalid timelines and ensure phases (coolDown
→ deepSleep → maintain → preWake) always advance monotonically.

In `@Sleepypod/Views/Schedule/SmartCurveView.swift`:
- Around line 501-513: The code currently sets scheduleManager.schedules and
then calls the API (APIBackend.current.createClient()/updateSchedules) but
always proceeds to clear isSaving, show the success animation/haptic (isSaving =
false, withAnimation { showSuccess = true }, Haptics.heavy(), sleep,
withAnimation { showSuccess = false }) even when the API throws; change the flow
so that scheduleManager.schedules is only updated with the API result (assign
the result of try await api.updateSchedules(schedules) to
scheduleManager.schedules) and move the success UI/haptic block (isSaving =
false, showSuccess animation, Haptics.heavy(), sleep, hide success) into the
success path after the await, and ensure the catch block sets isSaving = false
and does not trigger showSuccess (or returns/throws) so failure does not show
“Applied!”.

---

Nitpick comments:
In `@Sleepypod/Views/Schedule/SmartCurveView.swift`:
- Around line 503-504: The view is directly calling
APIBackend.current.createClient() and then api.updateSchedules(schedules), which
bypasses the injected ScheduleManager; instead, add or use a ScheduleManager API
boundary method (e.g., ScheduleManager.updateSchedules(_:), or a clearly named
wrapper like performRemoteUpdate(schedules:)) that encapsulates creating the API
client and performing api.updateSchedules(schedules), and replace the view call
with scheduleManager.updateSchedules(schedules) so the networking is owned by
ScheduleManager and the view remains testable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9bb83235-4739-41ee-a596-2575e1fdf784

📥 Commits

Reviewing files that changed from the base of the PR and between aef491c and 043b988.

📒 Files selected for processing (6)
  • Sleepypod.xcodeproj/project.pbxproj
  • Sleepypod/Models/SleepCurve.swift
  • Sleepypod/Services/ScheduleManager.swift
  • Sleepypod/Views/Schedule/DaySelectorView.swift
  • Sleepypod/Views/Schedule/RangeSlider.swift
  • Sleepypod/Views/Schedule/SmartCurveView.swift

Comment on lines +49 to +58
let coolRoom = baseTempF - minTempF // how much cooling is available
let warmRoom = maxTempF - baseTempF // how much warming is available

func scale(_ offset: Int) -> Int {
if offset < 0 {
// Cooling: scale to available cool range
return -min(abs(offset) * coolRoom / 10, coolRoom)
} else if offset > 0 {
// Warming: scale to available warm range
return min(offset * warmRoom / 8, warmRoom)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Clamp warm/cool capacity to non-negative values before scaling.

With current math, minTempF > 80 or maxTempF < 80 makes coolRoom/warmRoom negative, which can invert warming/cooling offsets (wrong curve direction).

Proposed fix
-        let baseTempF = 80
-        let coolRoom = baseTempF - minTempF  // how much cooling is available
-        let warmRoom = maxTempF - baseTempF  // how much warming is available
+        let baseTempF = 80
+        let normalizedMin = min(minTempF, maxTempF)
+        let normalizedMax = max(minTempF, maxTempF)
+        let coolRoom = max(0, baseTempF - normalizedMin)  // how much cooling is available
+        let warmRoom = max(0, normalizedMax - baseTempF)  // how much warming is available
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sleepypod/Models/SleepCurve.swift` around lines 49 - 58, The scale behavior
can invert when coolRoom or warmRoom is negative; clamp both available ranges to
non-negative values before using them in scale(_ offset: Int). Replace the
computed coolRoom and warmRoom with clamped versions (e.g., coolRoomClamped =
max(baseTempF - minTempF, 0) and warmRoomClamped = max(maxTempF - baseTempF, 0))
and use those in the scale function, and if a clamped range is zero ensure scale
returns 0 for that direction to avoid inverted or division-by-zero effects in
SleepCurve.scale(_:).

final class ScheduleManager {
var schedules: Schedules?
var selectedDay: DayOfWeek = .monday
var selectedDays: Set<DayOfWeek> = [.monday]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

selectedDays is introduced, but ScheduleManager write APIs still mutate only one day.

Line 9 adds multi-day state, but togglePowerSchedule, updateAlarmTime, updateBedtime, updatePhaseTemperature, and applyProfile still only update selectedDay. This creates inconsistent behavior versus bulk paths (for example, SmartCurve apply), and users can think multiple days are edited when only one is persisted.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sleepypod/Services/ScheduleManager.swift` at line 9, selectedDays was added
but write APIs still only mutate selectedDay, causing inconsistent persistence;
update togglePowerSchedule, updateAlarmTime, updateBedtime,
updatePhaseTemperature, and applyProfile to apply their changes for every day in
selectedDays (e.g., iterate selectedDays and call the existing per-day
write/persist logic or extract that per-day write into a helper and invoke it
for each DayOfWeek), ensuring UI state updates reflect the full set and using
the same persistence/notification path so multi-day edits are actually saved for
each day rather than only selectedDay.

Comment on lines +12 to 17
if isSelected && scheduleManager.selectedDays.count > 1 {
scheduleManager.selectedDays.remove(day)
} else if !isSelected {
scheduleManager.selectedDays.insert(day)
}
scheduleManager.selectedDay = day
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep selectedDay synchronized with selectedDays after deselection.

Line 17 runs unconditionally. When a day is removed at Line 13, selectedDay can remain set to a non-selected day.

Proposed fix
-                    if isSelected && scheduleManager.selectedDays.count > 1 {
-                        scheduleManager.selectedDays.remove(day)
-                    } else if !isSelected {
-                        scheduleManager.selectedDays.insert(day)
-                    }
-                    scheduleManager.selectedDay = day
+                    if isSelected && scheduleManager.selectedDays.count > 1 {
+                        scheduleManager.selectedDays.remove(day)
+                        if scheduleManager.selectedDay == day {
+                            scheduleManager.selectedDay =
+                                DayOfWeek.weekdays.first(where: { scheduleManager.selectedDays.contains($0) }) ?? day
+                        }
+                    } else if !isSelected {
+                        scheduleManager.selectedDays.insert(day)
+                        scheduleManager.selectedDay = day
+                    } else {
+                        scheduleManager.selectedDay = day
+                    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sleepypod/Views/Schedule/DaySelectorView.swift` around lines 12 - 17, The bug
is that scheduleManager.selectedDay is set unconditionally which can leave
selectedDay pointing to a deselected day; in the DaySelectorView handler that
currently mutates scheduleManager.selectedDays and then sets
scheduleManager.selectedDay, change the logic so selectedDay is only set when a
day is being selected, and when a day is being removed (in the branch that calls
scheduleManager.selectedDays.remove(day)) update scheduleManager.selectedDay to
a valid value (e.g., set it to nil or to a remaining selectedDays.first) to keep
scheduleManager.selectedDay synchronized with scheduleManager.selectedDays.

Comment on lines +12 to +17
private var span: Double { range.upperBound - range.lowerBound }

var body: some View {
GeometryReader { geo in
let width = geo.size.width - thumbSize

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In SwiftUI DragGesture, is DragGesture.Value.location reported in the local coordinate space of the view that owns the gesture, and what is the recommended way to map thumb drag to a parent track value?

💡 Result:

Yes. DragGesture reports value.location (and startLocation) in the gesture’s coordinateSpace, which defaults to .local—i.e., the local coordinate space of the view the gesture is attached to. The initializer’s default is coordinateSpace: .local. [1] And “local” means the coordinates are relative to that gesture-owning view (vs .global). [2]

Recommended pattern for a thumb-on-track (slider) mapping

  1. Put a named coordinate space on the track (the parent you want coordinates relative to), e.g. .coordinateSpace(name: "track").
  2. Build the thumb’s drag as DragGesture(..., coordinateSpace: .named("track")), so value.location.x is in track coordinates, even though the gesture is attached to the thumb. [1]
  3. Convert position → value using the track width from GeometryReader, then clamp:
struct MySlider: View {
  `@Binding` var value: Double   // in 0...1

  var body: some View {
    GeometryReader { geo in
      let w = geo.size.width
      let x = CGFloat(value) * w

      ZStack(alignment: .leading) {
        Capsule().frame(height: 4)

        Circle()
          .frame(width: 28, height: 28)
          .offset(x: x - 14) // center on x
          .gesture(
            DragGesture(minimumDistance: 0, coordinateSpace: .named("track"))
              .onChanged { g in
                let t = min(max(0, g.location.x), w) / w
                value = Double(t)
              }
          )
      }
      .coordinateSpace(name: "track")
    }
    .frame(height: 28)
  }
}

This avoids “thumb-local” coordinates and makes the math stable even if the thumb moves. [1]

Sources: [1] [2]


🏁 Script executed:

fd -t f RangeSlider.swift

Repository: sleepypod/ios

Length of output: 99


🏁 Script executed:

cat -n Sleepypod/Views/Schedule/RangeSlider.swift

Repository: sleepypod/ios

Length of output: 3563


Range slider drag math has coordinate space and divide-by-zero hazards.

The DragGesture on the thumbs uses the default .local coordinate space (attached to offset thumb views), but the math treats value.location.x as track-relative. This can cause discontinuous jumps and incorrect mappings, especially during drag. Additionally, divisions by step, span, and width lack guards—if step is set to 0, span is 0 (point range), or layout yields width ≤ 0, the math produces invalid states (NaN/Inf).

Apply a named coordinate space (e.g., .coordinateSpace(name: "track") on the track, DragGesture(coordinateSpace: .named("track")) on thumbs) and add guards for zero-valued divisors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sleepypod/Views/Schedule/RangeSlider.swift` around lines 12 - 17, The drag
math for the thumbs uses local coordinates and unguarded divisions which can
produce jumps and NaN/Inf; update the track view produced inside GeometryReader
to set a named coordinate space (e.g., .coordinateSpace(name: "track")), switch
the thumbs' DragGesture to DragGesture(coordinateSpace: .named("track")) so
value.location.x is track-relative, and add defensive guards around all divisors
used in the mapping logic (check step != 0, span = range.upperBound -
range.lowerBound != 0, and width = geo.size.width - thumbSize > 0) inside
whatever functions/closures compute the normalized position and step-snapped
value (refer to span, range, thumbSize, and the DragGesture handlers); if any
divisor is invalid, early-return or clamp to a well-defined fallback (current
bound or min/max) to avoid NaN/Inf and discontinuous jumps.

ng and others added 3 commits March 15, 2026 21:14
- Lines inset 30pt from left to avoid Y-axis legend overlap
- Wide tap target (±15pt) for easier dragging
- Lines clamp to chart bounds
- X-axis uses .hour() format — shows "10 PM", "2 AM" etc.
- Removed haptics from drag onChange (too frequent, causes lag)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Lines now use chartOverlay + proxy.position(forY:) to map offset
  to exact chart pixel coordinates. No more manual GeometryReader math
  that didn't account for axis label padding.
- Drag uses proxy.value(atY:) to convert touch position back to offset
- Lines start at the plot area edge (after Y-axis), end at right edge
- Min line bisects the chart at exactly the min temp offset
- X-axis uses 2-hour stride to show all hours including pre-midnight
  (10 PM, 12 AM, 2 AM, 4 AM, 6 AM, 8 AM)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Drag lines:
- 1pt solid at 40% opacity (was 2pt at 60%) — chart visible underneath
- Labels removed from lines — temp shown as chips above chart

Temp chips:
- Between time pickers and intensity: ❄️ 68°F — 🔥 86°F
- Colored capsule pills with icons
- "Drag lines on chart" hint

Phase labels:
- Vertical rule marks at each phase transition on the chart
- Labels at top: "Wind Down", "Fall Asleep", "Deep Sleep", etc.
- Color-matched to phase (warm=orange, cool=blue, deep=dark blue)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
Sleepypod/Views/Schedule/SmartCurveView.swift (3)

328-344: Unused label element in phases tuple.

The third element of each tuple (e.g., "+warm", "cool") is never used in the rendering. Either remove it or display it to provide additional context to users.

♻️ Option 1: Remove unused data
-        let phases: [(String, Color, String)] = [
-            ("Wind Down", Theme.warming, "+warm"),
-            ("Fall Asleep", Theme.cooling, "cool"),
-            ("Deep Sleep", Color(hex: "2563eb"), "coldest"),
-            ("Pre-Wake", Theme.amber, "+warm"),
+        let phases: [(String, Color)] = [
+            ("Wind Down", Theme.warming),
+            ("Fall Asleep", Theme.cooling),
+            ("Deep Sleep", Color(hex: "2563eb")),
+            ("Pre-Wake", Theme.amber),
         ]
         return HStack(spacing: 12) {
-            ForEach(phases, id: \.0) { name, color, label in
+            ForEach(phases, id: \.0) { name, color in
🤖 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 328 - 344, The
phases array in phaseLegend currently stores tuples (String, Color, String) but
the third element (label) is unused in the ForEach rendering; either remove the
third element from phases and change the tuple type to (String, Color) and adapt
the ForEach closure to unpack only (name, color), or use the label value in the
HStack (e.g., add a Text(label) with appropriate styling). Update the phases
declaration and the ForEach closure in phaseLegend accordingly so that the tuple
shape and its destructuring match (or the label is rendered) to eliminate the
unused value.

292-313: Remove unused phaseRanges computed property and PhaseRange struct.

The phaseRanges computed property is defined but never used. Remove both the property and the associated PhaseRange struct unless intended for future functionality.

🤖 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 292 - 313, Remove
the unused PhaseRange struct and the phaseRanges computed property from
SmartCurveView: delete the private struct PhaseRange and the private var
phaseRanges: [PhaseRange] { ... } declaration (which references phaseColor and
groups curve by \.phase) so no unused types or properties remain; if any future
usage is intended, instead add a FIXME/TODO comment or mark them
fileprivate/internal until they are referenced.

372-374: Remove unused state variables healthSynced and healthError or wire them to UI feedback.

Both variables are set in importFromHealth() but never read anywhere in the view's body. Either display error messages to users via an alert when healthError is non-nil, or remove these unused variables entirely.

🤖 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 372 - 374, The
view currently sets healthSynced and healthError inside importFromHealth() but
never uses them in the view body; either remove the unused `@State` properties
(healthSynced, healthError) and their assignments in importFromHealth(), or
surface healthError to the UI by wiring it to an Alert (e.g. add an .alert
modifier on the SmartCurveView using a Binding like .constant(healthError !=
nil) or a Bool computed from healthError, present the healthError string in the
alert message, and clear healthError when the alert is dismissed; if you choose
to keep healthSynced, bind it to visible UI feedback (a Text, badge, or disabled
state) so its value is read by the view body.
🤖 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/SmartCurveView.swift`:
- Line 114: The force-unwrap of proxy.plotFrame can crash; change the
GeometryReader closure to safely unwrap proxy.plotFrame (e.g., use if let
plotFrame = proxy.plotFrame or guard let plotFrame = proxy.plotFrame else {
return EmptyView() }) and then compute let plotArea = geo[plotFrame] inside that
safe block so you never index geo with a nil rect; update references to plotArea
usage inside SmartCurveView's GeometryReader closure accordingly.
- Around line 484-487: The guard in SmartCurveView that checks
scheduleManager.schedules silently returns when nil, leaving the UI with no
feedback; modify the Save/Apply handler (where the guard using
scheduleManager.schedules and isSaving is set) to set an error state instead of
returning silently: add or reuse a boolean alert state (e.g., showErrorAlert)
and an optional errorMessage property on SmartCurveView, populate errorMessage
with a descriptive message when scheduleManager.schedules is nil, set
showErrorAlert = true and reset isSaving = false, and ensure the View presents
an alert bound to showErrorAlert to notify the user that schedules are
unavailable.
- Around line 370-376: The importFromHealth() function is implemented but never
wired into the UI; hook it up to a user action (or remove it). Locate
importFromHealth() and add a call from the intensity selector handler (reference
the intensity selector control at line ~87) or from the Apply button action
handler (reference the Apply button action method around line ~158) so that
selecting/importing triggers importFromHealth(); alternatively remove
importFromHealth() and any HealthKit-related state (e.g., healthError) if the
feature is not intended to be exposed.

---

Nitpick comments:
In `@Sleepypod/Views/Schedule/SmartCurveView.swift`:
- Around line 328-344: The phases array in phaseLegend currently stores tuples
(String, Color, String) but the third element (label) is unused in the ForEach
rendering; either remove the third element from phases and change the tuple type
to (String, Color) and adapt the ForEach closure to unpack only (name, color),
or use the label value in the HStack (e.g., add a Text(label) with appropriate
styling). Update the phases declaration and the ForEach closure in phaseLegend
accordingly so that the tuple shape and its destructuring match (or the label is
rendered) to eliminate the unused value.
- Around line 292-313: Remove the unused PhaseRange struct and the phaseRanges
computed property from SmartCurveView: delete the private struct PhaseRange and
the private var phaseRanges: [PhaseRange] { ... } declaration (which references
phaseColor and groups curve by \.phase) so no unused types or properties remain;
if any future usage is intended, instead add a FIXME/TODO comment or mark them
fileprivate/internal until they are referenced.
- Around line 372-374: The view currently sets healthSynced and healthError
inside importFromHealth() but never uses them in the view body; either remove
the unused `@State` properties (healthSynced, healthError) and their assignments
in importFromHealth(), or surface healthError to the UI by wiring it to an Alert
(e.g. add an .alert modifier on the SmartCurveView using a Binding like
.constant(healthError != nil) or a Bool computed from healthError, present the
healthError string in the alert message, and clear healthError when the alert is
dismissed; if you choose to keep healthSynced, bind it to visible UI feedback (a
Text, badge, or disabled state) so its value is read by the view body.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c93fee97-33a0-4a4f-ad5c-5554e79dc603

📥 Commits

Reviewing files that changed from the base of the PR and between 043b988 and 1c95487.

📒 Files selected for processing (1)
  • Sleepypod/Views/Schedule/SmartCurveView.swift

Comment thread Sleepypod/Views/Schedule/SmartCurveView.swift
Comment on lines +370 to +376
// MARK: - HealthKit Import

private func importFromHealth() {
guard HKHealthStore.isHealthDataAvailable() else {
healthError = "Health data not available on this device"
return
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if importFromHealth is called anywhere
rg -n "importFromHealth" --type swift

Repository: sleepypod/ios

Length of output: 144


🏁 Script executed:

# Get more context around the function definition
sed -n '360,400p' Sleepypod/Views/Schedule/SmartCurveView.swift | cat -n

Repository: sleepypod/ios

Length of output: 1975


🏁 Script executed:

# Search for any UI elements (buttons, menu items) in SmartCurveView that might relate to HealthKit
rg -n "health|Health|import|Import" Sleepypod/Views/Schedule/SmartCurveView.swift -i | head -30

Repository: sleepypod/ios

Length of output: 958


🏁 Script executed:

# Check if there are any methods with common iOS patterns that might trigger this (buttons, gestures, etc.)
rg -n "@IBAction|\.onTapGesture|Button|action:|selector:" Sleepypod/Views/Schedule/SmartCurveView.swift

Repository: sleepypod/ios

Length of output: 113


🏁 Script executed:

# Check what those two buttons do
sed -n '87,95p' Sleepypod/Views/Schedule/SmartCurveView.swift | cat -n

Repository: sleepypod/ios

Length of output: 604


🏁 Script executed:

sed -n '158,166p' Sleepypod/Views/Schedule/SmartCurveView.swift | cat -n

Repository: sleepypod/ios

Length of output: 469


importFromHealth() is never called. The HealthKit import functionality is fully implemented but has no UI trigger—neither the intensity selector (line 87) nor the apply button (line 158) invoke this function. Either connect it to a button or menu option, or remove if no longer needed.

🤖 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 370 - 376, The
importFromHealth() function is implemented but never wired into the UI; hook it
up to a user action (or remove it). Locate importFromHealth() and add a call
from the intensity selector handler (reference the intensity selector control at
line ~87) or from the Apply button action handler (reference the Apply button
action method around line ~158) so that selecting/importing triggers
importFromHealth(); alternatively remove importFromHealth() and any
HealthKit-related state (e.g., healthError) if the feature is not intended to be
exposed.

Comment on lines +484 to +487
guard var schedules = scheduleManager.schedules else {
isSaving = false
return
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Silent failure when schedules is nil.

If scheduleManager.schedules is nil, the function returns silently without any user feedback. The user sees the button change briefly but gets no indication that the operation failed.

🐛 Proposed fix to add user feedback
         Task {
             guard var schedules = scheduleManager.schedules else {
                 isSaving = false
+                // Consider showing an error state or alert here
+                Log.general.error("Cannot apply smart curve: schedules not loaded")
                 return
             }

For a better UX, consider adding an error alert state and showing it when schedules is unavailable.

🤖 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 484 - 487, The
guard in SmartCurveView that checks scheduleManager.schedules silently returns
when nil, leaving the UI with no feedback; modify the Save/Apply handler (where
the guard using scheduleManager.schedules and isSaving is set) to set an error
state instead of returning silently: add or reuse a boolean alert state (e.g.,
showErrorAlert) and an optional errorMessage property on SmartCurveView,
populate errorMessage with a descriptive message when scheduleManager.schedules
is nil, set showErrorAlert = true and reset isSaving = false, and ensure the
View presents an alert bound to showErrorAlert to notify the user that schedules
are unavailable.

ng and others added 9 commits March 15, 2026 21:40
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Y-axis now auto-scales to the curve's min/max offset ±3 padding.
So a 80-85°F range (offset 0 to +5) shows -3 to +8 instead of
-20 to +20. The curve fills the chart height.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Chart clipped with rounded rect — curve can't overflow into text below
- Y-axis uses .automatic(desiredCount: 5) so labels adapt to the
  current range instead of fixed -8/-4/0/4/8 values
- Narrow ranges like 80-85°F now show useful increments (79, 81, 83, 85)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replaced draggable overlay lines with:
- Clean rule marks inside the chart (blue=min, orange=max, 1pt solid)
- Centered stepper controls below: [-] ❄️ 68°F [+] | [-] 🔥 86°F [+]
- 1°F per tap, haptic feedback, bounded so they can't cross

Chart is now clean — no overlay gestures, no drag handling.
Lines move as you tap the steppers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Chart split into two drag zones:
- Drag in top half → adjusts max temp (orange line)
- Drag in bottom half → adjusts min temp (blue line)
- Steppers below still work for precise 1°F taps
- Both update the same state — lines move in sync

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Drag:
- Single rectangle covers full plot area (was two offset zones)
- Uses startLocation to determine which line: bottom half = min, top half = max
- Both min and max now respond to drag

Layout:
- Phase legend moved above temp steppers (was below Apply)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ng ng merged commit 69e357c into main Mar 16, 2026
1 of 2 checks passed
@github-actions
Copy link
Copy Markdown

🎉 This PR is included in version 1.0.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Smart sleep curve: auto-generate temperature schedule from bedtime + wake time

1 participant