Skip to content
Open
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
15 changes: 14 additions & 1 deletion PulseLoop/Views/RecordViews.swift
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,20 @@ struct ActivityDetailView: View {
}
}
} else {
EmptyStateView(title: "Workout not found", body: "This session is no longer in local storage.")
// Stale route (e.g. a demo-data reseed wiped the session a pushed id points at):
// fill the screen with a clear fallback plus an explicit way back, so a missing
// workout is never a blank, dead-end page.
ContentUnavailableView {
Label("Workout not found", systemImage: "figure.run.circle")
} description: {
Text("This workout is no longer in local storage. It may have been deleted or replaced.")
} actions: {
Button("Back to Dashboard") { dismiss() }
.font(PulseFont.callout)
.foregroundStyle(PulseColors.accent)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(PulseColors.background)
}
}

Expand Down
32 changes: 21 additions & 11 deletions PulseLoop/Views/RootViews.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ struct RootAppView: View {
@Query private var profiles: [UserProfile]
@State private var path = NavigationPath()
@State private var didFinishForcedOnboarding = false
/// Launch-arg handling must run at most once per process: `.task` re-fires when the
/// root Group swaps content (onboarding → main tabs), and a second reseed would wipe
/// the sessions that routes already in `path` point at, stranding stale detail pages.
@State private var didRunLaunchArgs = false

private var forceOnboardingForTesting: Bool {
#if DEBUG
Expand Down Expand Up @@ -37,17 +41,23 @@ struct RootAppView: View {
// Demo data is opt-in: load it from Settings → "Reseed demo data", or via the
// `-seedDemo YES` launch arg (test tooling only). Normal launches start empty.
.task {
if UserDefaults.standard.bool(forKey: "seedDemo") {
SeedData.clearAll(modelContext)
SeedData.seedDemo(modelContext, completeOnboarding: true)
}
// Test tooling: deep-link straight to a seeded workout's detail (route map).
if UserDefaults.standard.bool(forKey: "openWorkout"),
let session = ActivityRepository.sessions(context: modelContext).first(where: { $0.status == .finished && $0.useGps }) {
path.append(AppRoute.activityDetail(session.id))
}
if UserDefaults.standard.bool(forKey: "openRecord") {
path.append(AppRoute.recordSelect)
if !didRunLaunchArgs {
didRunLaunchArgs = true
if UserDefaults.standard.bool(forKey: "seedDemo") {
SeedData.clearAll(modelContext)
SeedData.seedDemo(modelContext, completeOnboarding: true)
}
// Test tooling: deep-link straight to a seeded workout's detail (route map).
if UserDefaults.standard.bool(forKey: "openWorkout"),
let session = ActivityRepository.sessions(context: modelContext).first(where: { $0.status == .finished && $0.useGps }) {
// Start from a clean stack so Back always lands on the dashboard —
// never on a stale detail route left over from an earlier push.
path = NavigationPath()
path.append(AppRoute.activityDetail(session.id))
}
if UserDefaults.standard.bool(forKey: "openRecord") {
path.append(AppRoute.recordSelect)
}
}
// Re-attach to an in-progress workout left running across launches.
liveWorkout.recover()
Expand Down