feat(KB-129): dashboard performance optimizations

- Fix SSE hook cleanup to prevent memory leaks and stale connections
- Cap agent log memory and optimize batch log processing
- Memoize Board, Column, and TaskCard with custom comparator to reduce re-renders
- Stabilize column task arrays and preserve pagination across live updates
- Add TaskCardBadge component for PR/issue state display
- Remove deprecated GitHub polling code and archive functionality from core store
This commit is contained in:
gsxdsm
2026-03-30 11:26:55 -07:00
parent d147428430
commit a7de078fef
21 changed files with 1269 additions and 276 deletions

View File

@@ -1,8 +1,8 @@
import type { Task, TaskDetail, TaskCreateInput, Column as ColumnType } from "@kb/core";
import type { Task, TaskDetail, Column as ColumnType } from "@kb/core";
import { COLUMNS } from "@kb/core";
import { Column } from "./Column";
import type { ToastType } from "../hooks/useToast";
import { useState, useMemo } from "react";
import { useState, useMemo, useCallback, useRef } from "react";
interface BoardProps {
tasks: Task[];
@@ -24,8 +24,36 @@ interface BoardProps {
searchQuery?: string;
}
function sortTasksForColumn(tasks: Task[]): Task[] {
return [...tasks].sort((a, b) => {
if (a.columnMovedAt && b.columnMovedAt) {
return b.columnMovedAt.localeCompare(a.columnMovedAt);
}
if (a.columnMovedAt && !b.columnMovedAt) return -1;
if (!a.columnMovedAt && b.columnMovedAt) return 1;
return a.createdAt.localeCompare(b.createdAt);
});
}
function areTaskArraysEqual(previous: Task[], next: Task[]): boolean {
if (previous.length !== next.length) return false;
return previous.every((task, index) => task === next[index]);
}
export function Board({ tasks, maxConcurrent, onMoveTask, onOpenDetail, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onArchiveTask, onUnarchiveTask, searchQuery = "" }: BoardProps) {
const [archivedCollapsed, setArchivedCollapsed] = useState(true);
const tasksByColumnCacheRef = useRef<Record<ColumnType, Task[]>>({
triage: [],
todo: [],
"in-progress": [],
"in-review": [],
done: [],
archived: [],
});
const handleToggleArchivedCollapse = useCallback(() => {
setArchivedCollapsed((current) => !current);
}, []);
// Filter tasks based on search query (matches id, title, or description)
const filteredTasks = useMemo(() => {
@@ -39,25 +67,38 @@ export function Board({ tasks, maxConcurrent, onMoveTask, onOpenDetail, addToast
);
}, [tasks, searchQuery]);
// Keep per-column array identities stable for unchanged columns so React.memo(Column)
// can skip sibling rerenders during unrelated task updates.
const tasksByColumn = useMemo(() => {
const nextGrouped = Object.fromEntries(
COLUMNS.map((column) => [column, [] as Task[]]),
) as Record<ColumnType, Task[]>;
for (const task of filteredTasks) {
nextGrouped[task.column].push(task);
}
const previousGrouped = tasksByColumnCacheRef.current;
const stableGrouped = {} as Record<ColumnType, Task[]>;
for (const column of COLUMNS) {
const sortedTasks = sortTasksForColumn(nextGrouped[column]);
stableGrouped[column] = areTaskArraysEqual(previousGrouped[column], sortedTasks)
? previousGrouped[column]
: sortedTasks;
}
tasksByColumnCacheRef.current = stableGrouped;
return stableGrouped;
}, [filteredTasks]);
return (
<main className="board" id="board">
{COLUMNS.map((col) => (
<Column
key={col}
column={col}
tasks={filteredTasks
.filter((t) => t.column === col)
.sort((a, b) => {
// Tasks with columnMovedAt sort descending (most recent first)
// Tasks without it (legacy) fall to the bottom, sorted by createdAt ascending
if (a.columnMovedAt && b.columnMovedAt) {
return b.columnMovedAt.localeCompare(a.columnMovedAt);
}
if (a.columnMovedAt && !b.columnMovedAt) return -1;
if (!a.columnMovedAt && b.columnMovedAt) return 1;
return a.createdAt.localeCompare(b.createdAt);
})}
allTasks={tasks}
tasks={tasksByColumn[col]}
maxConcurrent={maxConcurrent}
onMoveTask={onMoveTask}
onOpenDetail={onOpenDetail}
@@ -68,7 +109,7 @@ export function Board({ tasks, maxConcurrent, onMoveTask, onOpenDetail, addToast
onUnarchiveTask={onUnarchiveTask}
{...(col === "triage" ? { onQuickCreate, onNewTask } : {})}
{...(col === "in-review" ? { autoMerge, onToggleAutoMerge } : {})}
{...(col === "archived" ? { collapsed: archivedCollapsed, onToggleCollapse: () => setArchivedCollapsed(!archivedCollapsed) } : {})}
{...(col === "archived" ? { collapsed: archivedCollapsed, onToggleCollapse: handleToggleArchivedCollapse } : {})}
/>
))}
</main>