Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
ec79eee
init; capabilities
austinkimchi Apr 5, 2026
4bb291f
init
austinkimchi Apr 5, 2026
fc0b89a
merge remote-tracking branch 'origin/main' into feat-watch-wake-onset
austinkimchi Apr 7, 2026
ba4836c
update: workflow for any iOS simulator
austinkimchi Apr 7, 2026
d5db52a
revert "update: workflow for any iOS simulator"
austinkimchi Apr 7, 2026
3a03c84
Fix: wake window not properly syncing; make default window editable
james-008 Apr 9, 2026
c62077b
ix: wake window not properly syncing; make default window editable
james-008 Apr 9, 2026
b1081ea
Revert "Fix: wake window not properly syncing; make default window ed…
james-008 Apr 9, 2026
e75b639
Revert "ix: wake window not properly syncing; make default window edi…
james-008 Apr 9, 2026
d330cba
Merge branch 'main' into feat-watch-wake-onset
austinkimchi Apr 9, 2026
ee9edaa
fix: missing behavior store causing build fail
austinkimchi Apr 9, 2026
a12cd43
fix: behaviorstore reference
austinkimchi Apr 9, 2026
d6c231e
add: no data handler
austinkimchi Apr 26, 2026
b8daf57
update: make AI determine icons
austinkimchi Apr 26, 2026
1bccf2c
add: AI disclaimer footer text
austinkimchi Apr 26, 2026
d1af7ae
add: reset services for ease of backend reset (demo)
austinkimchi Apr 26, 2026
3b3cdbf
fix: nights syncing with backend
austinkimchi Apr 26, 2026
675a152
fix: icon recommendation return
austinkimchi Apr 26, 2026
4d79a9d
fix: background contrast make text hard to read in input field
austinkimchi Apr 26, 2026
1115e11
fix: chat fallback to creating new thread and trying again
austinkimchi Apr 26, 2026
16fe432
fix: chat readability is harsh; add: dark mode support
austinkimchi Apr 29, 2026
45c4376
fix: background permission and failed watch sync
austinkimchi Apr 29, 2026
ef60ae0
fix: sync; add: debug refresh receommendation
austinkimchi Apr 29, 2026
4ca8d4b
update: hide Quick Action section; unused cards for now
austinkimchi Apr 30, 2026
0f6303d
update: faster polling and logging
austinkimchi Apr 30, 2026
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
409 changes: 409 additions & 0 deletions SleepFocus.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion SleepFocus/Components/Chat/ChatInputArea.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,6 @@ struct ChatInputArea: View {
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
.background(Color.appBackground)
.background(Color.appChatSurface)
}
}
2 changes: 1 addition & 1 deletion SleepFocus/Components/Chat/ChatMainView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ struct ChatMainView: View {
}
.padding(.top, 12)
.padding(.bottom, 12)
.background(Color.appBackground)
.background(Color.appChatSurface)
}
.background(Color.appBackground)
}
Expand Down
2 changes: 1 addition & 1 deletion SleepFocus/Components/Chat/ChatMessageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ struct ChatMessageView: View {
}

private var backgroundColor: Color {
message.sender == .user ? .systemBlue : Color.appFieldBackground
message.sender == .user ? .systemBlue : .appChatSurface
}

private var alignment: Alignment {
Expand Down
39 changes: 37 additions & 2 deletions SleepFocus/Components/Chat/ChatViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ final class ChatViewModel: ObservableObject {

guard !Task.isCancelled, self.sessionID == sessionID else { return }

if result.invalidateThread {
threadId = nil
}
if let resolvedThreadId = result.threadId, !resolvedThreadId.isEmpty {
threadId = resolvedThreadId
}
Expand All @@ -109,6 +112,7 @@ final class ChatViewModel: ObservableObject {
) async -> ReplyEnvelope {
let fallback =
"I'm having trouble reaching the AI model right now, but I can still help summarize your recent sleep and focus trends."
let hasExistingThread = !(existingThreadId?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true)

do {
let accessToken = try await authManager.validAccessToken()
Expand All @@ -127,10 +131,40 @@ final class ChatViewModel: ObservableObject {

return ReplyEnvelope(
replyText: reply.isEmpty ? fallback : reply,
threadId: resolvedThreadId
threadId: resolvedThreadId,
invalidateThread: false
)
} catch {
return ReplyEnvelope(replyText: fallback, threadId: existingThreadId)
let recoveryAction = ChatAPIService.shared.threadRecoveryAction(
for: error,
hadExistingThread: hasExistingThread
)
guard recoveryAction == .retryWithFreshThread else {
return ReplyEnvelope(replyText: fallback, threadId: existingThreadId, invalidateThread: false)
}

do {
let accessToken = try await authManager.validAccessToken()
let freshThreadId = try await resolveThreadId(
accessToken: accessToken,
dayUtc: dayUtc,
existingThreadId: nil
)
let retryReply = try await ChatAPIService.shared.sendMessage(
accessToken: accessToken,
threadId: freshThreadId,
message: text,
dayUtc: dayUtc
)

return ReplyEnvelope(
replyText: retryReply.isEmpty ? fallback : retryReply,
threadId: freshThreadId,
invalidateThread: false
)
} catch {
return ReplyEnvelope(replyText: fallback, threadId: nil, invalidateThread: true)
}
}
}

Expand Down Expand Up @@ -177,4 +211,5 @@ private struct QueuedMessage {
private struct ReplyEnvelope {
let replyText: String
let threadId: String?
let invalidateThread: Bool
}
4 changes: 2 additions & 2 deletions SleepFocus/Components/Chat/QuickPromptsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ struct QuickPromptsView: View {
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 14)
.padding(.vertical, 12)
.background(Color.appFieldBackground)
.background(Color.appChatSurface)
.cornerRadius(16)
.overlay(
RoundedRectangle(cornerRadius: 16)
Expand All @@ -40,6 +40,6 @@ struct QuickPromptsView: View {
}
}
.padding(.horizontal, 16)
.background(Color.appBackground)
.background(Color.appChatSurface)
}
}
7 changes: 4 additions & 3 deletions SleepFocus/Components/Home/HomeMainView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@ struct HomeMainView: View {
maxScore: homeSummaryStore.summary?.sleepQuality.maxScore,
sleepDuration: homeSummaryStore.sleepMetrics?.formattedAsleepDuration,
status: homeSummaryStore.summary?.sleepQuality.status,
isNoData: homeSummaryStore.summary?.sleepQuality.source?.lowercased() == "no_healthkit_sleep",
selectedDate: selectedDate,
navigateTo: navigationManager.navigateTo
)

SmartAlarmCard(
isEnabled: sleepPreferences.smartAlarmEnabled,
isSmartAlarmMode: sleepPreferences.smartAlarmMode == .bedtime,
targetTime: sleepPreferences.currentSmartAlarmTriggerDisplay,
targetTime: sleepPreferences.smartAlarmMode == .automatic
? sleepPreferences.automaticModePrimaryDisplay
: sleepPreferences.currentSmartAlarmTriggerDisplay,
overrideAlarmTime: sleepPreferences.date(for: sleepPreferences.smartAlarmWakeTimeMinutes),
onOverrideAlarmChange: { updatedDate in
sleepPreferences.updateSmartAlarmWakeTime(updatedDate)
Expand All @@ -50,8 +53,6 @@ struct HomeMainView: View {
selectedDate: selectedDate,
navigateTo: navigationManager.navigateTo
)

QuickActionsCard(navigateTo: navigationManager.navigateTo)
}
.padding(.horizontal, 16)
.padding(.bottom, 20)
Expand Down
44 changes: 36 additions & 8 deletions SleepFocus/Components/Home/RecommendationsCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,36 @@ struct RecommendationsCard: View {
let selectedDate: Date
let navigateTo: (String, Any?) -> Void

private var actionTexts: [String] {
private var displayDetails: [RecommendationDetail] {
if let recommendationDetails, !recommendationDetails.isEmpty {
return recommendationDetails
.map(\.action)
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
.filter { !$0.action.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
}

return (recommendations ?? [])
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
.filter { !$0.isEmpty }
.enumerated()
.map { index, action in
RecommendationDetail(
recommendationId: "fallback_\(index)",
action: action,
reason: "",
icon: nil,
tags: nil,
multiQuestion: nil,
response: nil
)
}
}

private var displayRecommendations: [Recommendation] {
let icons = ["moon.zzz.fill", "sun.max.fill", "figure.walk"]
let colors: [Color] = [.systemIndigo, .systemOrange, .systemGreen]

return actionTexts.prefix(3).enumerated().map { index, text in
return displayDetails.prefix(3).enumerated().map { index, detail in
Recommendation(
icon: icons[index % icons.count],
text: text,
icon: Self.safeIcon(detail.icon),
text: detail.action,
color: colors[index % colors.count]
)
}
Expand Down Expand Up @@ -153,6 +162,25 @@ struct RecommendationsCard: View {
formatter.unitsStyle = .short
return formatter
}()

private static func safeIcon(_ icon: String?) -> String {
let allowed = Set([
"moon.zzz.fill",
"sun.max.fill",
"bed.double.fill",
"clock.fill",
"figure.walk",
"cup.and.saucer.fill",
"wind",
"thermometer",
"brain.head.profile",
"sparkles",
])
guard let icon, allowed.contains(icon) else {
return "sparkles"
}
return icon
}
}

private struct RecommendationsSkeletonView: View {
Expand Down
5 changes: 5 additions & 0 deletions SleepFocus/Components/Home/SleepQualityCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ struct SleepQualityCard: View {
let maxScore: Double?
let sleepDuration: String?
let status: String?
let isNoData: Bool
let selectedDate: Date
let navigateTo: (String, Any?) -> Void

Expand Down Expand Up @@ -82,6 +83,10 @@ struct SleepQualityCard: View {
}

private func getStatusText() -> String {
if isNoData {
return "No Sleep Data"
}

if let status, !status.isEmpty {
return status
}
Expand Down
77 changes: 77 additions & 0 deletions SleepFocus/Components/Settings/DeveloperSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@ struct DeveloperSection: View {
@EnvironmentObject var notificationSettings: NotificationSettingsStore
let navigateTo: (String, Any?) -> Void
let clearLocalScoreFetchStorage: () -> Void
let clearRemoteSleepScoreArtifacts: () async throws -> Void
@State private var showClearStorageConfirmation = false
@State private var showClearStorageSuccess = false
@State private var showClearRemoteConfirmation = false
@State private var showClearRemoteSuccess = false
@State private var isClearingRemote = false
@State private var remoteClearErrorMessage: String?
@State private var showTestAlarmScheduled = false

var body: some View {
Expand Down Expand Up @@ -50,6 +55,17 @@ struct DeveloperSection: View {
action: { showClearStorageConfirmation = true }
)

Divider()
.padding(.leading, 60)

SettingsRow(
title: isClearingRemote ? "Clearing Backend Sleep Score..." : "Clear Backend Sleep Score",
action: {
guard !isClearingRemote else { return }
showClearRemoteConfirmation = true
}
)

if showTestAlarmScheduled {
HStack(spacing: 8) {
Image(systemName: "alarm.fill")
Expand Down Expand Up @@ -77,6 +93,34 @@ struct DeveloperSection: View {
.padding(.vertical, 12)
.background(Color.appCardSecondaryBackground)
}

if showClearRemoteSuccess {
HStack(spacing: 8) {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(Color(hex: "34C759"))
Text("Backend sleep score and recommendations cleared.")
.font(.system(size: 13, weight: .medium))
.foregroundColor(.appSecondaryText)
Spacer()
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
.background(Color.appCardSecondaryBackground)
}

if let remoteClearErrorMessage {
HStack(spacing: 8) {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(Color(hex: "FF9500"))
Text(remoteClearErrorMessage)
.font(.system(size: 13, weight: .medium))
.foregroundColor(.appSecondaryText)
Spacer()
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
.background(Color.appCardSecondaryBackground)
}
}
.background(Color.appCardBackground)
.cornerRadius(12)
Expand All @@ -90,6 +134,14 @@ struct DeveloperSection: View {
} message: {
Text("This removes local sync state and HTTP cache used during score-fetch testing.")
}
.alert("Clear backend sleep score?", isPresented: $showClearRemoteConfirmation) {
Button("Cancel", role: .cancel) {}
Button("Clear", role: .destructive) {
clearRemoteScoreArtifacts()
}
} message: {
Text("This keeps raw HealthKit data, but removes generated scoring, recommendation, chat-context, feedback, and pipeline artifacts so they can be rebuilt.")
}
#else
EmptyView()
#endif
Expand All @@ -112,6 +164,31 @@ struct DeveloperSection: View {
}
}

private func clearRemoteScoreArtifacts() {
Task {
await MainActor.run {
isClearingRemote = true
showClearRemoteSuccess = false
remoteClearErrorMessage = nil
}

do {
try await clearRemoteSleepScoreArtifacts()
await MainActor.run {
showClearRemoteSuccess = true
}
} catch {
await MainActor.run {
remoteClearErrorMessage = error.localizedDescription
}
}

await MainActor.run {
isClearingRemote = false
}
}
}

private static let timeFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "h:mm:ss a"
Expand Down
11 changes: 8 additions & 3 deletions SleepFocus/Components/Settings/SettingsMainView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import SwiftUI

struct SettingsMainView: View {
@EnvironmentObject var navigationManager: NavigationManager
@EnvironmentObject var authManager: AuthenticationManager
@EnvironmentObject var notificationSettings: NotificationSettingsStore
@EnvironmentObject var appearanceStore: AppearanceStore
@StateObject private var healthAuth = HealthAuth()
Expand Down Expand Up @@ -34,7 +35,8 @@ struct SettingsMainView: View {

DeveloperSection(
navigateTo: navigationManager.navigateTo,
clearLocalScoreFetchStorage: clearLocalScoreFetchStorage
clearLocalScoreFetchStorage: clearLocalScoreFetchStorage,
clearRemoteSleepScoreArtifacts: clearRemoteSleepScoreArtifacts
)

SignOutSection()
Expand All @@ -53,9 +55,12 @@ struct SettingsMainView: View {
private func clearLocalScoreFetchStorage() {
DeveloperStorageTools.clearScoreFetchStorage()
}
}


private func clearRemoteSleepScoreArtifacts() async throws {
try await DeveloperAPIService.clearSleepScoreArtifacts(authManager: authManager)
DeveloperStorageTools.clearHomeSummaryCache()
}
}



Expand Down
6 changes: 5 additions & 1 deletion SleepFocus/Extensions/ColorExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ extension Color {
static let appSecondaryText = Color(uiColor: .secondaryLabel)
static let appSeparator = Color(uiColor: .separator)
static let appFieldBackground = Color(uiColor: .secondarySystemBackground)
static let appChatSurface = Color(uiColor: UIColor { traitCollection in
traitCollection.userInterfaceStyle == .dark
? .secondarySystemBackground
: .white
})
static let appRecommendationHighlightStart = Color(uiColor: UIColor { traitCollection in
traitCollection.userInterfaceStyle == .dark
? UIColor(red: 0.27, green: 0.24, blue: 0.15, alpha: 1.0)
Expand Down Expand Up @@ -95,6 +100,5 @@ extension Color {






Loading
Loading