diff --git a/apps/web/app/api/internal/notes/[taskId]/read/route.ts b/apps/web/app/api/internal/notes/[taskId]/read/route.ts index e03f5c0..77ab0d4 100644 --- a/apps/web/app/api/internal/notes/[taskId]/read/route.ts +++ b/apps/web/app/api/internal/notes/[taskId]/read/route.ts @@ -25,6 +25,9 @@ export async function POST(_request: Request, { params }: RouteContext) { }, select: { id: true, + note: true, + summaryUpdatedAt: true, + taskUpdatedAt: true, project: { select: { companyId: true } }, }, }) @@ -39,20 +42,32 @@ export async function POST(_request: Request, { params }: RouteContext) { return notFound("Review reader agent not found.") } - await prisma.taskReadMarker.upsert({ - where: { - taskId_agentId_status: { + const readAt = new Date() + + await prisma.$transaction(async (tx) => { + if (!task.summaryUpdatedAt) { + await tx.task.update({ + where: { id: task.id }, + data: { summaryUpdatedAt: task.taskUpdatedAt }, + }) + } + + await tx.taskReadMarker.upsert({ + where: { + taskId_agentId_status: { + taskId: task.id, + agentId: reviewReader.id, + status: Status.done, + }, + }, + create: { taskId: task.id, agentId: reviewReader.id, status: Status.done, + readAt, }, - }, - create: { - taskId: task.id, - agentId: reviewReader.id, - status: Status.done, - }, - update: { readAt: new Date() }, + update: { readAt }, + }) }) return NextResponse.json({ diff --git a/apps/web/app/api/internal/notes/route.ts b/apps/web/app/api/internal/notes/route.ts index 6948d8a..2d687c6 100644 --- a/apps/web/app/api/internal/notes/route.ts +++ b/apps/web/app/api/internal/notes/route.ts @@ -49,6 +49,7 @@ export async function GET(request: NextRequest) { status: true, note: true, summaryUpdatedAt: true, + taskUpdatedAt: true, assigned: { select: { id: true, @@ -74,7 +75,9 @@ export async function GET(request: NextRequest) { const unreadNotes = notes.filter((task) => { const readAt = task.readMarkers[0]?.readAt - return !readAt || !task.summaryUpdatedAt || readAt < task.summaryUpdatedAt + const summaryUpdatedAt = task.summaryUpdatedAt ?? task.taskUpdatedAt + + return !readAt || readAt < summaryUpdatedAt }) return NextResponse.json({ @@ -85,7 +88,7 @@ export async function GET(request: NextRequest) { name: task.name, status: task.status, note: task.note ?? "", - summaryUpdatedAt: task.summaryUpdatedAt?.toISOString() ?? null, + summaryUpdatedAt: (task.summaryUpdatedAt ?? task.taskUpdatedAt).toISOString(), assigned: task.assigned, project: task.project, })), diff --git a/apps/web/app/dashboard/notes/page.tsx b/apps/web/app/dashboard/notes/page.tsx index d777820..608f6a3 100644 --- a/apps/web/app/dashboard/notes/page.tsx +++ b/apps/web/app/dashboard/notes/page.tsx @@ -43,6 +43,7 @@ export default async function NotesPage({ searchParams }: NotesPageProps) { status: true, note: true, summaryUpdatedAt: true, + taskUpdatedAt: true, assigned: { select: { id: true, @@ -69,7 +70,9 @@ export default async function NotesPage({ searchParams }: NotesPageProps) { const unreadNotes = notes.filter((task) => { const readAt = task.readMarkers[0]?.readAt - return !readAt || !task.summaryUpdatedAt || readAt < task.summaryUpdatedAt + const summaryUpdatedAt = task.summaryUpdatedAt ?? task.taskUpdatedAt + + return !readAt || readAt < summaryUpdatedAt }) return ( @@ -96,7 +99,7 @@ export default async function NotesPage({ searchParams }: NotesPageProps) { name: task.name, status: task.status, note: task.note ?? "", - summaryUpdatedAt: task.summaryUpdatedAt?.toISOString() ?? null, + summaryUpdatedAt: (task.summaryUpdatedAt ?? task.taskUpdatedAt).toISOString(), assigned: task.assigned, project: task.project, }))} diff --git a/prisma/migrations/20260511062000_add_task_dependencies_and_summary_updated_at/migration.sql b/prisma/migrations/20260511062000_add_task_dependencies_and_summary_updated_at/migration.sql index 5508da9..990f53a 100644 --- a/prisma/migrations/20260511062000_add_task_dependencies_and_summary_updated_at/migration.sql +++ b/prisma/migrations/20260511062000_add_task_dependencies_and_summary_updated_at/migration.sql @@ -16,6 +16,5 @@ CREATE INDEX "TaskDependency_dependencyTaskId_idx" ON "TaskDependency"("dependen ALTER TABLE "TaskDependency" ADD CONSTRAINT "TaskDependency_blockedTaskId_fkey" FOREIGN KEY ("blockedTaskId") REFERENCES "Task"("id") ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE "TaskDependency" ADD CONSTRAINT "TaskDependency_dependencyTaskId_fkey" FOREIGN KEY ("dependencyTaskId") REFERENCES "Task"("id") ON DELETE CASCADE ON UPDATE CASCADE; -UPDATE "Task" -SET "summaryUpdatedAt" = CURRENT_TIMESTAMP -WHERE "note" IS NOT NULL; +-- summaryUpdatedAt is intentionally left null here. A later migration backfills +-- it from taskUpdatedAt after that deterministic freshness column exists. diff --git a/prisma/migrations/20260512210500_backfill_summary_updated_at/migration.sql b/prisma/migrations/20260512210500_backfill_summary_updated_at/migration.sql new file mode 100644 index 0000000..4acd834 --- /dev/null +++ b/prisma/migrations/20260512210500_backfill_summary_updated_at/migration.sql @@ -0,0 +1,8 @@ +-- Backfill deterministic summary timestamps for legacy completed task notes. +-- Some done tasks were created with note text before summaryUpdatedAt existed or was +-- consistently written. Use the task's last update timestamp instead of CURRENT_TIMESTAMP +-- so existing read markers newer than the legacy note stay read. +UPDATE "Task" +SET "summaryUpdatedAt" = "taskUpdatedAt" +WHERE "note" IS NOT NULL + AND "summaryUpdatedAt" IS NULL;