Fix meal plan not populating on first login and add pull-to-refresh sync

Rewrite MealPlanSyncManager.performSync() (renamed from performInitialSync) to
discover _mealPlanAssignment metadata from all server recipes, not just locally-
known ones. On first sync all recipes are checked; on subsequent syncs only
recipes modified since lastMealPlanSyncDate are fetched (max 5 concurrent).

Trigger meal plan sync from pull-to-refresh on both the recipe and meal plan
tabs, and from the "Refresh all" toolbar button.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 11:40:31 +01:00
parent c8d9ab7397
commit 1f7f19c74b
9 changed files with 108 additions and 36 deletions

View File

@@ -101,7 +101,7 @@ class GroceryStateSyncManager {
// MARK: - Initial Sync
/// Pushes any local-only items and reconciles server items on app launch.
func performInitialSync() async {
func performSync() async {
guard let appState, let groceryManager else { return }
await loadPendingRemovals()
@@ -208,7 +208,7 @@ class GroceryStateSyncManager {
// MARK: - Pending Removal Tracking
/// Records a recipe ID whose grocery items were fully removed, so that
/// `performInitialSync` can push the deletion even after the key disappears
/// `performSync` can push the deletion even after the key disappears
/// from `groceryDict`.
func trackPendingRemoval(recipeId: String) {
pendingRemovalRecipeIds.insert(recipeId)

View File

@@ -60,27 +60,75 @@ class MealPlanSyncManager {
mealPlanManager.reconcileFromServer(serverAssignment: serverAssignment, recipeId: recipeId, recipeName: recipeName)
}
// MARK: - Initial Sync
// MARK: - Full Sync
func performInitialSync() async {
func performSync() async {
guard let appState, let mealPlanManager else { return }
let recipeIds = Array(mealPlanManager.entriesByDate.values.flatMap { $0 }.map(\.recipeId))
let uniqueIds = Array(Set(recipeIds))
for recipeId in uniqueIds {
guard let recipeIdInt = Int(recipeId) else { continue }
// Phase 1: Push locally-known meal plan state
let localRecipeIds = Array(Set(
mealPlanManager.entriesByDate.values.flatMap { $0 }.map(\.recipeId)
))
for recipeId in localRecipeIds {
await pushMealPlanState(forRecipeId: recipeId)
}
if let serverRecipe = await appState.getRecipe(id: recipeIdInt, fetchMode: .onlyServer) {
reconcileFromServer(
serverAssignment: serverRecipe.mealPlanAssignment,
recipeId: recipeId,
recipeName: serverRecipe.name
)
// Phase 2: Discover meal plan assignments from server
let allRecipes = await appState.getRecipes()
let lastSync = UserSettings.shared.lastMealPlanSyncDate
// Filter to recipes modified since last sync
let recipesToCheck: [Recipe]
if let lastSync {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
formatter.timeZone = TimeZone(secondsFromGMT: 0)
recipesToCheck = allRecipes.filter { recipe in
guard let dateStr = recipe.dateModified,
let date = formatter.date(from: dateStr) else { return true }
return date > lastSync
}
} else {
recipesToCheck = allRecipes // First sync: check all
}
// Fetch details concurrently (max 5 parallel)
await withTaskGroup(of: (String, String, MealPlanAssignment?)?.self) { group in
var iterator = recipesToCheck.makeIterator()
let maxConcurrent = 5
var active = 0
while active < maxConcurrent, let recipe = iterator.next() {
active += 1
group.addTask {
guard let detail = await appState.getRecipe(
id: recipe.recipe_id, fetchMode: .onlyServer
) else { return nil }
return (String(recipe.recipe_id), detail.name, detail.mealPlanAssignment)
}
}
for await result in group {
if let (recipeId, recipeName, assignment) = result,
let assignment, !assignment.dates.isEmpty {
mealPlanManager.reconcileFromServer(
serverAssignment: assignment,
recipeId: recipeId,
recipeName: recipeName
)
}
if let recipe = iterator.next() {
group.addTask {
guard let detail = await appState.getRecipe(
id: recipe.recipe_id, fetchMode: .onlyServer
) else { return nil }
return (String(recipe.recipe_id), detail.name, detail.mealPlanAssignment)
}
}
}
}
UserSettings.shared.lastMealPlanSyncDate = Date()
}
// MARK: - Merge Logic

View File

@@ -175,6 +175,12 @@ class UserSettings: ObservableObject {
}
}
@Published var lastMealPlanSyncDate: Date? {
didSet {
UserDefaults.standard.set(lastMealPlanSyncDate, forKey: "lastMealPlanSyncDate")
}
}
init() {
self.username = UserDefaults.standard.object(forKey: "username") as? String ?? ""
self.token = UserDefaults.standard.object(forKey: "token") as? String ?? ""
@@ -203,6 +209,7 @@ class UserSettings: ObservableObject {
self.categorySortAscending = UserDefaults.standard.object(forKey: "categorySortAscending") as? Bool ?? true
self.recipeSortMode = UserDefaults.standard.object(forKey: "recipeSortMode") as? String ?? RecipeSortMode.recentlyAdded.rawValue
self.recipeSortAscending = UserDefaults.standard.object(forKey: "recipeSortAscending") as? Bool ?? true
self.lastMealPlanSyncDate = UserDefaults.standard.object(forKey: "lastMealPlanSyncDate") as? Date
if authString == "" {
if token != "" && username != "" {