Skip to content
Merged
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
62 changes: 61 additions & 1 deletion PulseLoop/Persistence/SeedData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,27 @@ enum SeedData {
for block in blocks {
context.insert(SleepStageBlock(sessionId: session.id, startAt: block.startAt, startMinute: block.startMinute, durationMinutes: block.durationMinutes, stage: block.stage))
}

// A few recent days also get daytime nap(s) — separate sessions sharing the same waking
// day — so the Day-view sleep carousel (issue #59) has multiple sessions to page through.
for nap in napsForDay(i) {
guard let napStart = calendar.date(bySettingHour: nap.hour, minute: nap.minute, second: 0, of: dayDate) else { continue }
let napEnd = calendar.date(byAdding: .minute, value: nap.minutes, to: napStart) ?? napStart
let napBlocks = napStageBlocks(total: nap.minutes, start: napStart)
let napLight = napBlocks.filter { $0.stage == .light }.reduce(0) { $0 + $1.durationMinutes }
let napDeep = napBlocks.filter { $0.stage == .deep }.reduce(0) { $0 + $1.durationMinutes }
let napAwake = napBlocks.filter { $0.stage == .awake }.reduce(0) { $0 + $1.durationMinutes }
let napSummary = SleepSummary(
session: SleepSession(date: dayDate, startAt: napStart, endAt: napEnd, totalMinutes: nap.minutes),
lightMinutes: napLight, deepMinutes: napDeep, awakeMinutes: napAwake, blocks: napBlocks
)
let napScore = SleepScore.calculate(napSummary)
let napSession = SleepSession(date: dayDate, startAt: napStart, endAt: napEnd, totalMinutes: nap.minutes, score: napScore.score, syncedAt: napEnd)
context.insert(napSession)
for block in napBlocks {
context.insert(SleepStageBlock(sessionId: napSession.id, startAt: block.startAt, startMinute: block.startMinute, durationMinutes: block.durationMinutes, stage: block.stage))
}
}
}

// Several finished workouts across recent days (one today).
Expand Down Expand Up @@ -303,7 +324,46 @@ enum SeedData {
}
return blocks
}


/// A seeded daytime nap: local hour/minute it starts and how long it runs.
private struct SeedNap { let hour: Int; let minute: Int; let minutes: Int }

/// Naps to seed for the night `dayIndex` days ago. Kept sparse and varied so the sleep carousel
/// shows a mix of single-session days, a two-session day, and a three-session day. All start
/// before 7 PM so they stay on the same waking day as that morning's night session.
private static func napsForDay(_ dayIndex: Int) -> [SeedNap] {
switch dayIndex {
// Today is the richest day so the multi-session carousel is testable on the Day view (which
// shows only today until day navigation lands): night + 2 naps = 3 pages.
case 0: return [SeedNap(hour: 11, minute: 0, minutes: 45), SeedNap(hour: 15, minute: 30, minutes: 52)]
case 1: return [SeedNap(hour: 10, minute: 30, minutes: 40), SeedNap(hour: 16, minute: 0, minutes: 35)]
case 2: return [SeedNap(hour: 13, minute: 0, minutes: 66)]
case 5: return [SeedNap(hour: 15, minute: 15, minutes: 24)]
default: return []
}
}

/// A short, nap-shaped stage architecture (mostly light with a little deep, no REM/awake),
/// scaled to `total` minutes. Mirrors `stageBlocks` but tuned for a brief daytime sleep.
private static func napStageBlocks(total: Int, start: Date) -> [SleepStageBlock] {
let pattern: [(SleepStage, Int)] = [(.light, 14), (.deep, 18), (.light, 13)]
let referenceTotal = pattern.reduce(0) { $0 + $1.1 }
let scale = Double(total) / Double(referenceTotal)
let placeholder = UUID()
var cursor = start
var minute = 0
var blocks: [SleepStageBlock] = []
for (index, item) in pattern.enumerated() {
let duration = index == pattern.count - 1
? max(1, total - minute)
: max(1, Int((Double(item.1) * scale).rounded()))
blocks.append(SleepStageBlock(sessionId: placeholder, startAt: cursor, startMinute: minute, durationMinutes: duration, stage: item.0))
cursor = Calendar.current.date(byAdding: .minute, value: duration, to: cursor) ?? cursor
minute += duration
}
return blocks
}

/// Inserts a synthetic GPS loop (~60 points) around `origin` so the route map has data to draw
/// in the Simulator. The path is a gently wobbling closed loop, not a real recording.
private static func seedRoute(_ context: ModelContext, sessionId: UUID, start: Date, durationMinutes: Int, origin: (Double, Double)) {
Expand Down
Loading