diff --git a/web/src/app/api/sync/route.ts b/web/src/app/api/sync/route.ts index 95ff08c..b0b35f9 100644 --- a/web/src/app/api/sync/route.ts +++ b/web/src/app/api/sync/route.ts @@ -202,32 +202,44 @@ async function processOperation(op: SyncOperation, userId: string) { // If the incoming list is an inbox, check for an existing inbox and merge if (incomingIsInbox) { - const [existingInbox] = await db - .select() - .from(slLists) - .where(and(eq(slLists.userId, userId), eq(slLists.isInbox, true), isNull(slLists.deletedAt))); + await db.transaction(async (tx) => { + const [existingInbox] = await tx + .select() + .from(slLists) + .where(and(eq(slLists.userId, userId), eq(slLists.isInbox, true), isNull(slLists.deletedAt))); - if (existingInbox && existingInbox.id !== entityId) { - // Reassign all tasks from the old inbox to the new one - await db.update(slTasks) - .set({ listId: entityId, updatedAt: now }) - .where(and(eq(slTasks.listId, existingInbox.id), eq(slTasks.userId, userId))); - // Soft-delete the old inbox - await db.update(slLists) - .set({ deletedAt: now, updatedAt: now }) - .where(eq(slLists.id, existingInbox.id)); - } + if (existingInbox && existingInbox.id !== entityId) { + // Reassign all tasks (including subtasks) from the old inbox to the new one + await tx.update(slTasks) + .set({ listId: entityId, updatedAt: now }) + .where(and(eq(slTasks.listId, existingInbox.id), eq(slTasks.userId, userId))); + // Soft-delete the old inbox + await tx.update(slLists) + .set({ deletedAt: now, updatedAt: now }) + .where(eq(slLists.id, existingInbox.id)); + } + + await tx.insert(slLists).values({ + id: entityId, + userId, + name: d.name as string || 'Untitled', + color: d.color as string | undefined, + icon: d.icon as string | undefined, + position: d.position as number | undefined, + isInbox: incomingIsInbox, + }).onConflictDoNothing(); + }); + } else { + await db.insert(slLists).values({ + id: entityId, + userId, + name: d.name as string || 'Untitled', + color: d.color as string | undefined, + icon: d.icon as string | undefined, + position: d.position as number | undefined, + isInbox: incomingIsInbox, + }).onConflictDoNothing(); } - - await db.insert(slLists).values({ - id: entityId, - userId, - name: d.name as string || 'Untitled', - color: d.color as string | undefined, - icon: d.icon as string | undefined, - position: d.position as number | undefined, - isInbox: incomingIsInbox, - }).onConflictDoNothing(); } else if (action === 'update') { await verifyOwnership(slLists, entityId, userId); await db.update(slLists) diff --git a/web/src/db/seed.ts b/web/src/db/seed.ts index 4f48b0d..0632530 100644 --- a/web/src/db/seed.ts +++ b/web/src/db/seed.ts @@ -18,12 +18,10 @@ async function seed() { .from(slLists) .where(and(eq(slLists.userId, userId), eq(slLists.isInbox, true))); - // Use the same fixed inbox ID as the mobile app to avoid duplicates during sync - const INBOX_ID = '00000000-0000-0000-0000-000000000001'; - if (existing.length === 0) { + // Let the DB generate a random UUID — the sync endpoint handles + // inbox deduplication when mobile pushes its fixed-ID inbox. await db.insert(slLists).values({ - id: INBOX_ID, userId, name: 'Inbox', isInbox: true,