From 5b7efa2c182933b8307cfb4ec6e6e745e3ddce20 Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Sat, 21 Feb 2026 15:47:05 -0600 Subject: [PATCH] Fix 413 error - send individual tasks instead of bulk --- src/app/api/tasks/route.ts | 205 ++++++++++++++++++++++++------------- src/stores/useTaskStore.ts | 83 ++++++++++++--- 2 files changed, 202 insertions(+), 86 deletions(-) diff --git a/src/app/api/tasks/route.ts b/src/app/api/tasks/route.ts index 1ee0ab5..611a193 100644 --- a/src/app/api/tasks/route.ts +++ b/src/app/api/tasks/route.ts @@ -1,9 +1,36 @@ import { NextResponse } from "next/server"; -import { getData, saveData, type DataStore, type Task } from "@/lib/server/taskDb"; +import { getServiceSupabase } from "@/lib/supabase/client"; import { getAuthenticatedUser } from "@/lib/server/auth"; export const runtime = "nodejs"; +interface Task { + id: string; + title: string; + description?: string; + type: "idea" | "task" | "bug" | "research" | "plan"; + status: "open" | "todo" | "blocked" | "in-progress" | "review" | "validate" | "archived" | "canceled" | "done"; + priority: "low" | "medium" | "high" | "urgent"; + projectId: string; + sprintId?: string; + createdAt?: string; + updatedAt?: string; + createdById?: string; + createdByName?: string; + createdByAvatarUrl?: string; + updatedById?: string; + updatedByName?: string; + updatedByAvatarUrl?: string; + assigneeId?: string; + assigneeName?: string; + assigneeEmail?: string; + assigneeAvatarUrl?: string; + dueDate?: string; + comments?: unknown[]; + tags?: string[]; + attachments?: unknown[]; +} + // GET - fetch all tasks, projects, and sprints export async function GET() { try { @@ -11,15 +38,29 @@ export async function GET() { if (!user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - const data = await getData(); - return NextResponse.json(data); + + const supabase = getServiceSupabase(); + + const [{ data: projects }, { data: sprints }, { data: tasks }, { data: meta }] = await Promise.all([ + supabase.from("projects").select("*").order("created_at", { ascending: true }), + supabase.from("sprints").select("*").order("start_date", { ascending: true }), + supabase.from("tasks").select("*").order("created_at", { ascending: true }), + supabase.from("meta").select("value").eq("key", "lastUpdated").maybeSingle(), + ]); + + return NextResponse.json({ + projects: projects || [], + sprints: sprints || [], + tasks: tasks || [], + lastUpdated: Number(meta?.value ?? Date.now()), + }); } catch (error) { - console.error(">>> API GET: database error:", error); + console.error(">>> API GET error:", error); return NextResponse.json({ error: "Failed to fetch data" }, { status: 500 }); } } -// POST - create or update tasks, projects, or sprints +// POST - create or update a single task (lightweight) export async function POST(request: Request) { try { const user = await getAuthenticatedUser(); @@ -28,70 +69,83 @@ export async function POST(request: Request) { } const body = await request.json(); - const { task, tasks, projects, sprints } = body as { - task?: Task; - tasks?: Task[]; - projects?: DataStore["projects"]; - sprints?: DataStore["sprints"]; + const { task } = body as { task?: Task }; + + if (!task) { + return NextResponse.json({ error: "Missing task" }, { status: 400 }); + } + + const supabase = getServiceSupabase(); + const now = new Date().toISOString(); + + // Check if task exists + const { data: existing } = await supabase + .from("tasks") + .select("id") + .eq("id", task.id) + .maybeSingle(); + + const taskData = { + id: task.id, + title: task.title, + description: task.description || null, + type: task.type || "task", + status: task.status || "todo", + priority: task.priority || "medium", + project_id: task.projectId, + sprint_id: task.sprintId || null, + created_at: existing ? undefined : (task.createdAt || now), + updated_at: now, + created_by_id: existing ? undefined : (task.createdById || user.id), + created_by_name: existing ? undefined : (task.createdByName || user.name), + created_by_avatar_url: existing ? undefined : task.createdByAvatarUrl, + updated_by_id: user.id, + updated_by_name: user.name, + updated_by_avatar_url: user.avatarUrl, + assignee_id: task.assigneeId || null, + assignee_name: task.assigneeName || null, + assignee_email: task.assigneeEmail || null, + assignee_avatar_url: task.assigneeAvatarUrl || null, + due_date: task.dueDate || null, + comments: task.comments || [], + tags: task.tags || [], + attachments: task.attachments || [], }; - const data = await getData(); - - if (projects) data.projects = projects; - if (sprints) data.sprints = sprints; - - if (task) { - const existingIndex = data.tasks.findIndex((t) => t.id === task.id); - if (existingIndex >= 0) { - const existingTask = data.tasks[existingIndex]; - data.tasks[existingIndex] = { - ...existingTask, - ...task, - updatedAt: new Date().toISOString(), - updatedById: user.id, - updatedByName: user.name, - updatedByAvatarUrl: user.avatarUrl, - }; - } else { - data.tasks.push({ - ...task, - id: task.id || Date.now().toString(), - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - createdById: task.createdById || user.id, - createdByName: task.createdByName || user.name, - createdByAvatarUrl: task.createdByAvatarUrl || user.avatarUrl, - updatedById: user.id, - updatedByName: user.name, - updatedByAvatarUrl: user.avatarUrl, - assigneeId: task.assigneeId || user.id, - assigneeName: task.assigneeName || user.name, - assigneeEmail: task.assigneeEmail || user.email, - }); - } + let result; + if (existing) { + // Update existing + const { data, error } = await supabase + .from("tasks") + .update(taskData) + .eq("id", task.id) + .select() + .single(); + if (error) throw error; + result = data; + } else { + // Insert new + const { data, error } = await supabase + .from("tasks") + .insert(taskData) + .select() + .single(); + if (error) throw error; + result = data; } - if (tasks && Array.isArray(tasks)) { - data.tasks = tasks.map((entry) => ({ - ...entry, - createdById: entry.createdById || user.id, - createdByName: entry.createdByName || user.name, - createdByAvatarUrl: entry.createdByAvatarUrl || (entry.createdById === user.id ? user.avatarUrl : undefined), - updatedById: entry.updatedById || user.id, - updatedByName: entry.updatedByName || user.name, - updatedByAvatarUrl: entry.updatedByAvatarUrl || (entry.updatedById === user.id ? user.avatarUrl : undefined), - assigneeId: entry.assigneeId || undefined, - assigneeName: entry.assigneeName || undefined, - assigneeEmail: entry.assigneeEmail || undefined, - assigneeAvatarUrl: undefined, - })); - } + // Update lastUpdated + await supabase.from("meta").upsert({ + key: "lastUpdated", + value: String(Date.now()), + updated_at: now, + }); - const saved = await saveData(data); - return NextResponse.json({ success: true, data: saved }); - } catch (error) { - console.error(">>> API POST: database error:", error); - return NextResponse.json({ error: "Failed to save" }, { status: 500 }); + return NextResponse.json({ success: true, task: result }); + } catch (error: unknown) { + console.error(">>> API POST error:", error); + const message = error instanceof Error ? error.message : "Failed to save"; + return NextResponse.json({ error: message }, { status: 500 }); } } @@ -104,12 +158,23 @@ export async function DELETE(request: Request) { } const { id } = (await request.json()) as { id: string }; - const data = await getData(); - data.tasks = data.tasks.filter((t) => t.id !== id); - await saveData(data); + + const supabase = getServiceSupabase(); + const { error } = await supabase.from("tasks").delete().eq("id", id); + + if (error) throw error; + + // Update lastUpdated + await supabase.from("meta").upsert({ + key: "lastUpdated", + value: String(Date.now()), + updated_at: new Date().toISOString(), + }); + return NextResponse.json({ success: true }); - } catch (error) { - console.error(">>> API DELETE: database error:", error); - return NextResponse.json({ error: "Failed to delete" }, { status: 500 }); + } catch (error: unknown) { + console.error(">>> API DELETE error:", error); + const message = error instanceof Error ? error.message : "Failed to delete"; + return NextResponse.json({ error: message }, { status: 500 }); } } diff --git a/src/stores/useTaskStore.ts b/src/stores/useTaskStore.ts index a5beb3b..0c464cf 100644 --- a/src/stores/useTaskStore.ts +++ b/src/stores/useTaskStore.ts @@ -552,27 +552,67 @@ const removeCommentFromThread = (comments: Comment[], targetId: string): Comment replies: removeCommentFromThread(normalizeComments(comment.replies), targetId), })) -// Helper to sync to server -async function syncToServer(projects: Project[], tasks: Task[], sprints: Sprint[]) { - console.log('>>> syncToServer: saving', tasks.length, 'tasks,', projects.length, 'projects,', sprints.length, 'sprints') - const t2 = tasks.find(t => t.id === '2') - if (t2) console.log('>>> syncToServer: task 2 sprintId:', t2.sprintId) +// Helper to sync a single task to server (lightweight) +async function syncTaskToServer(task: Task) { + console.log('>>> syncTaskToServer: saving task', task.id, task.title) try { - const body = JSON.stringify({ projects, tasks, sprints }) - console.log('>>> syncToServer: sending body with keys:', Object.keys(JSON.parse(body))) const res = await fetch('/api/tasks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: body, + body: JSON.stringify({ task }), }) if (res.ok) { - const responseData = await res.json() - console.log('>>> syncToServer: saved successfully, server now has', responseData.data?.tasks?.length, 'tasks') + console.log('>>> syncTaskToServer: saved successfully') + return true + } else { + console.error('>>> syncTaskToServer: failed with status', res.status) + return false + } + } catch (error) { + console.error('>>> syncTaskToServer: Failed to sync:', error) + return false + } +} + +// Helper to delete a task from server +async function deleteTaskFromServer(taskId: string) { + console.log('>>> deleteTaskFromServer: deleting task', taskId) + try { + const res = await fetch('/api/tasks', { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: taskId }), + }) + if (res.ok) { + console.log('>>> deleteTaskFromServer: deleted successfully') + return true + } else { + console.error('>>> deleteTaskFromServer: failed with status', res.status) + return false + } + } catch (error) { + console.error('>>> deleteTaskFromServer: Failed to delete:', error) + return false + } +} + +// Legacy bulk sync (for projects/sprints only) +async function syncToServer(projects: Project[], tasks: Task[], sprints: Sprint[]) { + console.log('>>> syncToServer: legacy bulk sync - projects/sprints only') + // Only sync projects and sprints in bulk, tasks are handled individually + try { + const res = await fetch('/api/projects', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ projects, sprints }), + }) + if (res.ok) { + console.log('>>> syncToServer: saved projects/sprints successfully') } else { console.error('>>> syncToServer: failed with status', res.status) } } catch (error) { - console.error('>>> syncToServer: Failed to sync to server:', error) + console.error('>>> syncToServer: Failed to sync:', error) } } @@ -699,7 +739,8 @@ export const useTaskStore = create()( } set((state) => { const newTasks = [...state.tasks, newTask] - syncToServer(state.projects, newTasks, state.sprints) + // Sync individual task to server (lightweight) + syncTaskToServer(newTask) return { tasks: newTasks } }) }, @@ -724,7 +765,10 @@ export const useTaskStore = create()( ) const updatedTask = newTasks.find(t => t.id === id) console.log('updateTask: updated task:', updatedTask) - syncToServer(state.projects, newTasks, state.sprints) + // Sync individual task to server (lightweight) + if (updatedTask) { + syncTaskToServer(updatedTask) + } return { tasks: newTasks } }) }, @@ -732,7 +776,8 @@ export const useTaskStore = create()( deleteTask: (id) => { set((state) => { const newTasks = state.tasks.filter((t) => t.id !== id) - syncToServer(state.projects, newTasks, state.sprints) + // Delete individual task from server (lightweight) + deleteTaskFromServer(id) return { tasks: newTasks, selectedTaskId: state.selectedTaskId === id ? null : state.selectedTaskId, @@ -810,7 +855,10 @@ export const useTaskStore = create()( } : t ) - syncToServer(state.projects, newTasks, state.sprints) + const updatedTask = newTasks.find(t => t.id === taskId) + if (updatedTask) { + syncTaskToServer(updatedTask) + } return { tasks: newTasks } }) }, @@ -830,7 +878,10 @@ export const useTaskStore = create()( } : t ) - syncToServer(state.projects, newTasks, state.sprints) + const updatedTask = newTasks.find(t => t.id === taskId) + if (updatedTask) { + syncTaskToServer(updatedTask) + } return { tasks: newTasks } }) },