diff --git a/web/src/app/api/sync/route.ts b/web/src/app/api/sync/route.ts index aebdf44..95ff08c 100644 --- a/web/src/app/api/sync/route.ts +++ b/web/src/app/api/sync/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { db } from '@/db/client'; import { slLists, slTasks, slTags, slTaskTags } from '@/db/schema'; -import { eq, and, gte } from 'drizzle-orm'; +import { eq, and, gte, isNull } from 'drizzle-orm'; import { requireAuth, parseBody } from '@/lib/api'; import { rateLimit } from '@/lib/rateLimit'; import { syncPushSchema, type SyncOperation } from '@/lib/validators'; @@ -197,14 +197,36 @@ async function processOperation(op: SyncOperation, userId: string) { switch (entityType) { case 'list': { if (action === 'create') { + const d = (data as Record) || {}; + const incomingIsInbox = d.isInbox as boolean | undefined; + + // 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))); + + 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)); + } + } + await db.insert(slLists).values({ id: entityId, userId, - name: (data as Record)?.name as string || 'Untitled', - color: (data as Record)?.color as string | undefined, - icon: (data as Record)?.icon as string | undefined, - position: (data as Record)?.position as number | undefined, - isInbox: (data as Record)?.isInbox as boolean | undefined, + 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); diff --git a/web/src/db/seed.ts b/web/src/db/seed.ts index 4072b06..4f48b0d 100644 --- a/web/src/db/seed.ts +++ b/web/src/db/seed.ts @@ -18,8 +18,12 @@ 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) { await db.insert(slLists).values({ + id: INBOX_ID, userId, name: 'Inbox', isInbox: true,