From 81b9a586ed85c6fe27ddd4e0d8c57f58aad8946a Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Wed, 4 Mar 2026 16:25:16 +0530 Subject: [PATCH] fix(activity): use API route metadata for feed links --- frontend/src/app/activity/page.tsx | 178 +++++++++++++++++++++++++---- 1 file changed, 155 insertions(+), 23 deletions(-) diff --git a/frontend/src/app/activity/page.tsx b/frontend/src/app/activity/page.tsx index 10a8cb46..d9d884ac 100644 --- a/frontend/src/app/activity/page.tsx +++ b/frontend/src/app/activity/page.tsx @@ -89,9 +89,11 @@ type FeedItem = { actor_role: string | null; board_id: string | null; board_name: string | null; + board_href: string | null; task_id: string | null; task_title: string | null; title: string; + context_href: string | null; }; type TaskMeta = { @@ -99,6 +101,10 @@ type TaskMeta = { boardId: string | null; }; +type ActivityRouteParams = Record; + +const ACTIVITY_FEED_PATH = "/activity"; + const TASK_EVENT_TYPES = new Set([ "task.comment", "task.created", @@ -120,6 +126,73 @@ const formatShortTimestamp = (value: string) => { }); }; +const normalizeRouteParams = ( + params: ActivityEventRead["route_params"] | ActivityRouteParams | null | undefined, +): ActivityRouteParams => { + if (!params || typeof params !== "object") return {}; + return Object.entries(params).reduce((acc, [key, value]) => { + if (typeof value === "string" && value.length > 0) { + acc[key] = value; + } + return acc; + }, {}); +}; + +const buildRouteHref = ( + routeName: string | null | undefined, + routeParams: ActivityRouteParams, + fallback: { + eventId: string; + eventType: string; + createdAt: string; + taskId: string | null; + }, +): string => { + if (routeName === "board.approvals") { + const boardId = routeParams.boardId; + if (boardId) { + return `/boards/${encodeURIComponent(boardId)}/approvals`; + } + } + + if (routeName === "board") { + const boardId = routeParams.boardId; + if (boardId) { + const params = new URLSearchParams(); + Object.entries(routeParams).forEach(([key, value]) => { + if (key !== "boardId") params.set(key, value); + }); + const query = params.toString(); + return query + ? `/boards/${encodeURIComponent(boardId)}?${query}` + : `/boards/${encodeURIComponent(boardId)}`; + } + } + + const params = new URLSearchParams( + Object.keys(routeParams).length > 0 + ? routeParams + : { + eventId: fallback.eventId, + eventType: fallback.eventType, + createdAt: fallback.createdAt, + }, + ); + if (fallback.taskId && !params.has("taskId")) { + params.set("taskId", fallback.taskId); + } + return `${ACTIVITY_FEED_PATH}?${params.toString()}`; +}; + +const buildBoardHref = ( + routeParams: ActivityRouteParams, + boardId: string | null, +): string | null => { + const resolved = routeParams.boardId ?? boardId; + if (!resolved) return null; + return `/boards/${encodeURIComponent(resolved)}`; +}; + const feedItemElementId = (id: string): string => `activity-item-${id.replace(/[^a-zA-Z0-9_-]/g, "-")}`; @@ -219,11 +292,6 @@ const FeedCard = memo(function FeedCard({ }) { const message = (item.message ?? "").trim(); const authorAvatar = (item.actor_name[0] ?? "A").toUpperCase(); - const taskHref = - item.board_id && item.task_id - ? `/boards/${item.board_id}?taskId=${item.task_id}` - : null; - const boardHref = item.board_id ? `/boards/${item.board_id}` : null; return (
- {taskHref ? ( + {item.context_href ? ( {eventLabel(item.event_type)} - {boardHref && item.board_name ? ( + {item.board_href && item.board_name ? ( {item.board_name} @@ -424,12 +492,30 @@ export default function ActivityPage() { ); const mapTaskActivity = useCallback( - (event: ActivityEventRead): FeedItem | null => { + ( + event: ActivityEventRead, + fallbackBoardId: string | null = null, + ): FeedItem | null => { if (!isTaskEventType(event.event_type)) return null; const meta = event.task_id ? taskMetaByIdRef.current.get(event.task_id) : null; - const boardId = meta?.boardId ?? null; + const routeName = event.route_name ?? null; + const routeParams = normalizeRouteParams(event.route_params); + const taskId = event.task_id ?? routeParams.taskId ?? null; + const boardId = + meta?.boardId ?? + event.board_id ?? + routeParams.boardId ?? + fallbackBoardId ?? + null; + const fallbackRouteParams: ActivityRouteParams = {}; + if (boardId) fallbackRouteParams.boardId = boardId; + if (taskId) fallbackRouteParams.taskId = taskId; + const effectiveRouteParams = + Object.keys(routeParams).length > 0 ? routeParams : fallbackRouteParams; + const effectiveRouteName = + routeName ?? (boardId ? "board" : "activity"); const author = resolveAuthor(event.agent_id, currentUserDisplayName); return { id: `activity:${event.id}`, @@ -442,10 +528,17 @@ export default function ActivityPage() { actor_role: author.role, board_id: boardId, board_name: boardNameForId(boardId), - task_id: event.task_id ?? null, + board_href: buildBoardHref(effectiveRouteParams, boardId), + task_id: taskId, task_title: meta?.title ?? null, title: - meta?.title ?? (event.task_id ? "Unknown task" : "Task activity"), + meta?.title ?? (taskId ? "Unknown task" : "Task activity"), + context_href: buildRouteHref(effectiveRouteName, effectiveRouteParams, { + eventId: event.id, + eventType: event.event_type, + createdAt: event.created_at, + taskId, + }), }; }, [boardNameForId, currentUserDisplayName, resolveAuthor], @@ -457,6 +550,11 @@ export default function ActivityPage() { ? taskMetaByIdRef.current.get(comment.task_id) : null; const boardId = meta?.boardId ?? fallbackBoardId; + const taskId = comment.task_id ?? null; + const routeParams: ActivityRouteParams = {}; + if (boardId) routeParams.boardId = boardId; + if (taskId) routeParams.taskId = taskId; + routeParams.commentId = comment.id; const author = resolveAuthor(comment.agent_id, currentUserDisplayName); return { id: `comment:${comment.id}`, @@ -469,10 +567,17 @@ export default function ActivityPage() { actor_role: author.role, board_id: boardId, board_name: boardNameForId(boardId), - task_id: comment.task_id ?? null, + board_href: buildBoardHref(routeParams, boardId), + task_id: taskId, task_title: meta?.title ?? null, title: - meta?.title ?? (comment.task_id ? "Unknown task" : "Task activity"), + meta?.title ?? (taskId ? "Unknown task" : "Task activity"), + context_href: buildRouteHref("board", routeParams, { + eventId: comment.id, + eventType: "task.comment", + createdAt: comment.created_at, + taskId, + }), }; }, [boardNameForId, currentUserDisplayName, resolveAuthor], @@ -525,6 +630,8 @@ export default function ActivityPage() { const taskMeta = approval.task_id ? taskMetaByIdRef.current.get(approval.task_id) : null; + const routeParams: ActivityRouteParams = { boardId }; + const taskId = approval.task_id ?? null; return { id: `approval:${approval.id}:${kind}:${stamp}`, @@ -537,9 +644,16 @@ export default function ActivityPage() { actor_role: author.role, board_id: boardId, board_name: boardNameForId(boardId), - task_id: approval.task_id ?? null, + board_href: buildBoardHref(routeParams, boardId), + task_id: taskId, task_title: taskMeta?.title ?? null, title: `Approval · ${action}`, + context_href: buildRouteHref("board.approvals", routeParams, { + eventId: approval.id, + eventType: kind, + createdAt: stamp, + taskId, + }), }; }, [boardNameForId, currentUserDisplayName, resolveAuthor], @@ -553,6 +667,7 @@ export default function ActivityPage() { currentUserDisplayName, ); const command = content.startsWith("/"); + const routeParams: ActivityRouteParams = { boardId, panel: "chat" }; return { id: `chat:${memory.id}`, created_at: memory.created_at, @@ -564,9 +679,16 @@ export default function ActivityPage() { actor_role: null, board_id: boardId, board_name: boardNameForId(boardId), + board_href: buildBoardHref(routeParams, boardId), task_id: null, task_title: null, title: command ? "Board command" : "Board chat", + context_href: buildRouteHref("board", routeParams, { + eventId: memory.id, + eventType: command ? "board.command" : "board.chat", + createdAt: memory.created_at, + taskId: null, + }), }; }, [boardNameForId, currentUserDisplayName], @@ -618,6 +740,10 @@ export default function ActivityPage() { : kind === "agent.offline" ? `${agent.name} is offline.` : `${agent.name} updated (${humanizeStatus(nextStatus)}).`; + const boardId = agent.board_id ?? null; + const routeParams: ActivityRouteParams = boardId + ? { boardId } + : {}; return { id: `agent:${agent.id}:${isSnapshot ? "snapshot" : kind}:${stamp}`, @@ -628,11 +754,21 @@ export default function ActivityPage() { agent_id: agent.id, actor_name: agent.name, actor_role: roleFromAgent(agent), - board_id: agent.board_id ?? null, - board_name: boardNameForId(agent.board_id), + board_id: boardId, + board_name: boardNameForId(boardId), + board_href: buildBoardHref(routeParams, boardId), task_id: null, task_title: null, title: `Agent · ${agent.name}`, + context_href: + boardId === null + ? null + : buildRouteHref("board", routeParams, { + eventId: agent.id, + eventType: kind, + createdAt: stamp, + taskId: null, + }), }; }, [boardNameForId], @@ -871,12 +1007,8 @@ export default function ActivityPage() { updateTaskMeta(payload.task, boardId); } if (payload.activity) { - const mapped = mapTaskActivity(payload.activity); + const mapped = mapTaskActivity(payload.activity, boardId); if (mapped) { - if (!mapped.board_id) { - mapped.board_id = boardId; - mapped.board_name = boardNameForId(boardId); - } if (!mapped.task_title && payload.task?.title) { mapped.task_title = payload.task.title; mapped.title = payload.task.title;