fix: resolve duplicate inbox on web after mobile sync (#60)

When mobile syncs its inbox (fixed ID) to the web, check if an inbox
already exists for the user. If so, reassign tasks and soft-delete the
old inbox to prevent duplicates. Also harmonize seed.ts to use the same
fixed inbox ID as mobile.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
le king fu 2026-04-08 20:50:12 -04:00
parent 71ee702739
commit d9daf9eda4
2 changed files with 32 additions and 6 deletions

View file

@ -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<string, unknown>) || {};
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<string, unknown>)?.name as string || 'Untitled',
color: (data as Record<string, unknown>)?.color as string | undefined,
icon: (data as Record<string, unknown>)?.icon as string | undefined,
position: (data as Record<string, unknown>)?.position as number | undefined,
isInbox: (data as Record<string, unknown>)?.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);

View file

@ -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,