Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions apps/HeartCoach/Tests/StressViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -367,4 +367,58 @@ final class StressViewModelTests: XCTestCase {

XCTAssertEqual(vm.assessmentReadinessLevel, .recovering)
}

// MARK: - Hourly Display Filtering

func testVisibleHourlyPointsForDisplay_todayFiltersFutureHours() {
let vm = StressViewModel()
let calendar = Calendar.current
let now = calendar.date(
bySettingHour: 10,
minute: 15,
second: 0,
of: Date()
) ?? Date()
let today = calendar.startOfDay(for: now)

let points = (0..<24).map { hour in
HourlyStressPoint(
date: calendar.date(bySettingHour: hour, minute: 0, second: 0, of: today) ?? today,
hour: hour,
score: 50,
level: .balanced
)
}

let visible = vm.visibleHourlyPointsForDisplay(points, on: today, now: now)
XCTAssertEqual(visible.count, 11, "10 AM should show 0...10 only")
XCTAssertEqual(visible.last?.hour, 10)
}

func testVisibleHourlyPointsForDisplay_pastDayKeepsAllHours() {
let vm = StressViewModel()
let calendar = Calendar.current
let now = calendar.date(
bySettingHour: 10,
minute: 15,
second: 0,
of: Date()
) ?? Date()
let yesterday = calendar.date(byAdding: .day, value: -1, to: now) ?? now
let dayStart = calendar.startOfDay(for: yesterday)

let points = (0..<24).map { hour in
HourlyStressPoint(
date: calendar.date(bySettingHour: hour, minute: 0, second: 0, of: dayStart) ?? dayStart,
hour: hour,
score: 50,
level: .balanced
)
}

let visible = vm.visibleHourlyPointsForDisplay(points, on: dayStart, now: now)
XCTAssertEqual(visible.count, 24)
XCTAssertEqual(visible.first?.hour, 0)
XCTAssertEqual(visible.last?.hour, 23)
}
}
61 changes: 58 additions & 3 deletions apps/HeartCoach/iOS/ViewModels/StressViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ final class StressViewModel: ObservableObject {
}
#endif

// Ensure today is present so day-view stress never falls back to
// yesterday by default when the history query excludes the current day.
snapshots = await mergeWithTodaySnapshot(snapshots)

history = snapshots

// When coordinator is available, read pre-computed values
Expand Down Expand Up @@ -253,10 +257,14 @@ final class StressViewModel: ObservableObject {
selectedDayHourlyPoints = []
} else {
selectedDayForDetail = date
selectedDayHourlyPoints = engine.hourlyStressForDay(
let points = engine.hourlyStressForDay(
snapshots: history,
date: date
)
selectedDayHourlyPoints = visibleHourlyPointsForDisplay(
points,
on: date
)
}
}

Expand Down Expand Up @@ -582,9 +590,13 @@ final class StressViewModel: ObservableObject {
snapshots: history,
date: today
)
if !todayHourly.isEmpty {
let visibleTodayHourly = visibleHourlyPointsForDisplay(
todayHourly,
on: today
)
if !visibleTodayHourly.isEmpty {
hourlyReferenceDate = today
hourlyPoints = todayHourly
hourlyPoints = visibleTodayHourly
} else if let latestDate = history.map(\.date).max() {
hourlyReferenceDate = latestDate
hourlyPoints = engine.hourlyStressForDay(
Expand All @@ -601,6 +613,49 @@ final class StressViewModel: ObservableObject {
selectedDayHourlyPoints = []
}

/// Applies display-time filtering for hourly points.
///
/// For today, we only show up to the current local hour so future
/// buckets (e.g., 11 PM at 10 AM) are not shown as current data.
/// For past days, all 24 hourly buckets remain visible.
func visibleHourlyPointsForDisplay(
_ points: [HourlyStressPoint],
on date: Date,
now: Date = Date()
) -> [HourlyStressPoint] {
guard Calendar.current.isDate(date, inSameDayAs: now) else {
return points
}
let currentHour = Calendar.current.component(.hour, from: now)
return points.filter { $0.hour <= currentHour }
}

/// Adds or replaces today's snapshot so stress day-view calculations
/// are anchored to the current day when available.
private func mergeWithTodaySnapshot(_ snapshots: [HeartSnapshot]) async -> [HeartSnapshot] {
var merged = snapshots
let calendar = Calendar.current
let today = calendar.startOfDay(for: Date())

do {
let todaySnapshot = try await healthKitService.fetchTodaySnapshot()
if let existingIndex = merged.firstIndex(where: { calendar.isDate($0.date, inSameDayAs: today) }) {
merged[existingIndex] = todaySnapshot
} else {
merged.append(todaySnapshot)
}
} catch {
AppLogger.healthKit.warning("Today snapshot fetch failed: \(error.localizedDescription)")
#if targetEnvironment(simulator)
if !merged.contains(where: { calendar.isDateInToday($0.date) }) {
merged.append(MockData.mockTodaySnapshot)
}
#endif
}

return merged.sorted { $0.date < $1.date }
}

/// Learn sleep patterns from history.
private func learnPatterns() {
sleepPatterns = scheduler.learnSleepPatterns(from: history)
Expand Down
Loading