Signed-off-by: Max <ai-agent@topdoglabs.com>

This commit is contained in:
Max 2026-02-23 17:36:25 -06:00
parent 5c6bd134bd
commit c1ae51b1ec
4 changed files with 50 additions and 19 deletions

View File

@ -63,8 +63,8 @@ const TASK_PRIORITIES: Task["priority"][] = ["low", "medium", "high", "urgent"];
const SPRINT_STATUSES: Sprint["status"][] = ["planning", "active", "completed"]; const SPRINT_STATUSES: Sprint["status"][] = ["planning", "active", "completed"];
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
// Optimized field selection - fetch all fields needed for board and detail display // Field sets are split so board loads can avoid heavy attachment payloads.
const TASK_FIELDS = [ const TASK_BASE_FIELDS = [
"id", "id",
"title", "title",
"type", "type",
@ -80,10 +80,11 @@ const TASK_FIELDS = [
"due_date", "due_date",
"tags", "tags",
"comments", "comments",
"attachments",
"description", "description",
]; ];
const TASK_DETAIL_ONLY_FIELDS = ["attachments"];
class HttpError extends Error { class HttpError extends Error {
readonly status: number; readonly status: number;
readonly details?: Record<string, unknown>; readonly details?: Record<string, unknown>;
@ -271,7 +272,7 @@ function mapTaskRow(row: Record<string, unknown>, usersById: Map<string, UserPro
// GET - fetch all tasks, projects, and sprints // GET - fetch all tasks, projects, and sprints
// Uses lightweight fields for faster initial load // Uses lightweight fields for faster initial load
export async function GET() { export async function GET(request: Request) {
try { try {
const user = await getAuthenticatedUser(); const user = await getAuthenticatedUser();
if (!user) { if (!user) {
@ -279,7 +280,11 @@ export async function GET() {
} }
const supabase = getServiceSupabase(); const supabase = getServiceSupabase();
const includeFullTaskData = new URL(request.url).searchParams.get("include") === "detail";
const taskFieldSet = includeFullTaskData
? [...TASK_BASE_FIELDS, ...TASK_DETAIL_ONLY_FIELDS]
: TASK_BASE_FIELDS;
// Use Promise.all for parallel queries with optimized field selection // Use Promise.all for parallel queries with optimized field selection
const [ const [
{ data: projects, error: projectsError }, { data: projects, error: projectsError },
@ -289,7 +294,7 @@ export async function GET() {
] = await Promise.all([ ] = await Promise.all([
supabase.from("projects").select("id, name, description, color, created_at").order("created_at", { ascending: true }), supabase.from("projects").select("id, name, description, color, created_at").order("created_at", { ascending: true }),
supabase.from("sprints").select("id, name, goal, start_date, end_date, status, project_id, created_at").order("start_date", { ascending: true }), supabase.from("sprints").select("id, name, goal, start_date, end_date, status, project_id, created_at").order("start_date", { ascending: true }),
supabase.from("tasks").select(TASK_FIELDS.join(", ")).order("updated_at", { ascending: false }), supabase.from("tasks").select(taskFieldSet.join(", ")).order("updated_at", { ascending: false }),
supabase.from("users").select("id, name, email, avatar_url"), supabase.from("users").select("id, name, email, avatar_url"),
]); ]);
@ -307,7 +312,7 @@ export async function GET() {
return NextResponse.json({ return NextResponse.json({
projects: (projects || []).map((row) => mapProjectRow(row as Record<string, unknown>)), projects: (projects || []).map((row) => mapProjectRow(row as Record<string, unknown>)),
sprints: (sprints || []).map((row) => mapSprintRow(row as Record<string, unknown>)), sprints: (sprints || []).map((row) => mapSprintRow(row as Record<string, unknown>)),
tasks: ((taskRows as unknown as Record<string, unknown>[] | null) || []).map((row) => mapTaskRow(row, usersById, false)), tasks: ((taskRows as unknown as Record<string, unknown>[] | null) || []).map((row) => mapTaskRow(row, usersById, includeFullTaskData)),
currentUser: { currentUser: {
id: user.id, id: user.id,
name: user.name, name: user.name,

View File

@ -256,7 +256,7 @@ export default function TaskDetailPage() {
useEffect(() => { useEffect(() => {
if (!authReady) return if (!authReady) return
syncFromServer() syncFromServer({ includeFullTaskData: true })
}, [authReady, syncFromServer]) }, [authReady, syncFromServer])
useEffect(() => { useEffect(() => {
@ -393,7 +393,7 @@ export default function TaskDetailPage() {
} }
} }
const handleAddComment = () => { const handleAddComment = async () => {
if (!editedTask || !newComment.trim()) return if (!editedTask || !newComment.trim()) return
const commentAuthorId = getCurrentUserCommentAuthorId(currentUser) const commentAuthorId = getCurrentUserCommentAuthorId(currentUser)
@ -401,14 +401,28 @@ export default function TaskDetailPage() {
toast.error("You must be signed in to add a comment.") toast.error("You must be signed in to add a comment.")
return return
} }
setEditedTask({
const nextTask: Task = {
...editedTask, ...editedTask,
comments: [...getComments(editedTask.comments), buildComment(newComment.trim(), commentAuthorId)], comments: [...getComments(editedTask.comments), buildComment(newComment.trim(), commentAuthorId)],
}) }
setEditedTask(nextTask)
setNewComment("") setNewComment("")
const success = await updateTask(nextTask.id, {
...nextTask,
comments: getComments(nextTask.comments),
})
if (!success) {
toast.error("Failed to save comment", {
description: "Comment was added locally but could not sync to the server.",
duration: 5000,
})
}
} }
const handleAddReply = (parentId: string) => { const handleAddReply = async (parentId: string) => {
if (!editedTask) return if (!editedTask) return
const text = replyDrafts[parentId]?.trim() const text = replyDrafts[parentId]?.trim()
if (!text) return if (!text) return
@ -418,13 +432,26 @@ export default function TaskDetailPage() {
toast.error("You must be signed in to reply.") toast.error("You must be signed in to reply.")
return return
} }
setEditedTask({ const nextTask: Task = {
...editedTask, ...editedTask,
comments: addReplyToThread(getComments(editedTask.comments), parentId, buildComment(text, commentAuthorId)), comments: addReplyToThread(getComments(editedTask.comments), parentId, buildComment(text, commentAuthorId)),
}) }
setEditedTask(nextTask)
setReplyDrafts((prev) => ({ ...prev, [parentId]: "" })) setReplyDrafts((prev) => ({ ...prev, [parentId]: "" }))
setOpenReplyEditors((prev) => ({ ...prev, [parentId]: false })) setOpenReplyEditors((prev) => ({ ...prev, [parentId]: false }))
const success = await updateTask(nextTask.id, {
...nextTask,
comments: getComments(nextTask.comments),
})
if (!success) {
toast.error("Failed to save reply", {
description: "Reply was added locally but could not sync to the server.",
duration: 5000,
})
}
} }
const handleDeleteComment = (commentId: string) => { const handleDeleteComment = (commentId: string) => {

View File

@ -392,8 +392,6 @@ export async function revokeSession(token: string): Promise<void> {
} }
export async function getUserBySessionToken(token: string): Promise<AuthUser | null> { export async function getUserBySessionToken(token: string): Promise<AuthUser | null> {
await deleteExpiredSessions();
const supabase = getServiceSupabase(); const supabase = getServiceSupabase();
const tokenHash = hashSessionToken(token); const tokenHash = hashSessionToken(token);
const now = new Date().toISOString(); const now = new Date().toISOString();

View File

@ -90,7 +90,7 @@ interface TaskStore {
syncError: string | null syncError: string | null
// Sync actions // Sync actions
syncFromServer: () => Promise<void> syncFromServer: (options?: { includeFullTaskData?: boolean }) => Promise<void>
setCurrentUser: (user: Partial<UserProfile>) => void setCurrentUser: (user: Partial<UserProfile>) => void
// Project actions // Project actions
@ -326,11 +326,12 @@ export const useTaskStore = create<TaskStore>()(
lastSynced: null, lastSynced: null,
syncError: null, syncError: null,
syncFromServer: async () => { syncFromServer: async (options) => {
console.log('>>> syncFromServer START') console.log('>>> syncFromServer START')
set({ isLoading: true, syncError: null }) set({ isLoading: true, syncError: null })
try { try {
const res = await fetch('/api/tasks', { cache: 'no-store' }) const query = options?.includeFullTaskData ? '?include=detail' : ''
const res = await fetch(`/api/tasks${query}`, { cache: 'no-store' })
console.log('>>> syncFromServer: API response status:', res.status) console.log('>>> syncFromServer: API response status:', res.status)
if (!res.ok) { if (!res.ok) {
const errorPayload = await res.json().catch(() => ({})) const errorPayload = await res.json().catch(() => ({}))