perf(dashboard): slim task list + auto-archive stale done tasks
GET /api/tasks was returning ~69 MB of JSON per call (67.9 MB of agent logs across 1199 tasks), causing the dashboard to hang for 2+ minutes. - core: extend listTasks() with slim and includeArchived options - dashboard: GET /api/tasks now uses slim mode and excludes archived by default; ?includeArchived=1 opts in - frontend: lazy-load archived tasks when the archived column is first expanded via new useTasks.loadArchivedTasks() - engine: self-healing maintenance now auto-archives done tasks older than 48h (data stays in SQLite, column flips done -> archived) - tests: slim mode + includeArchived coverage in store.test.ts; routes.test.ts assertion updated for new args Also bundles in-progress test-setup noise filters and pre-existing QuickEntryBox/routes test work that was already modified locally. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,8 @@ interface BoardProps {
|
||||
onArchiveTask?: (id: string) => Promise<Task>;
|
||||
onUnarchiveTask?: (id: string) => Promise<Task>;
|
||||
onArchiveAllDone?: () => Promise<Task[]>;
|
||||
/** Lazy-load archived tasks. Called the first time the user expands the archived column. */
|
||||
onLoadArchivedTasks?: () => Promise<void>;
|
||||
searchQuery?: string;
|
||||
availableModels?: ModelInfo[];
|
||||
/**
|
||||
@@ -62,8 +64,9 @@ function areTaskArraysEqual(previous: Task[], next: Task[]): boolean {
|
||||
return previous.every((task, index) => task === next[index]);
|
||||
}
|
||||
|
||||
export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onOpenDetail, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onArchiveTask, onUnarchiveTask, onArchiveAllDone, searchQuery = "", availableModels, onPlanningMode, onSubtaskBreakdown, onOpenDetailWithTab, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, taskStuckTimeoutMs, onOpenMission }: BoardProps) {
|
||||
export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onOpenDetail, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onArchiveTask, onUnarchiveTask, onArchiveAllDone, onLoadArchivedTasks, searchQuery = "", availableModels, onPlanningMode, onSubtaskBreakdown, onOpenDetailWithTab, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, taskStuckTimeoutMs, onOpenMission }: BoardProps) {
|
||||
const [archivedCollapsed, setArchivedCollapsed] = useState(true);
|
||||
const archivedLoadedRef = useRef(false);
|
||||
const { fetchBatch } = useBatchBadgeFetch(projectId);
|
||||
const debounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
// Normalized search-active signal: trimmed and non-empty
|
||||
@@ -78,8 +81,15 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onOpenDetai
|
||||
});
|
||||
|
||||
const handleToggleArchivedCollapse = useCallback(() => {
|
||||
setArchivedCollapsed((current) => !current);
|
||||
}, []);
|
||||
setArchivedCollapsed((current) => {
|
||||
const next = !current;
|
||||
if (!next && !archivedLoadedRef.current && onLoadArchivedTasks) {
|
||||
archivedLoadedRef.current = true;
|
||||
void onLoadArchivedTasks();
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, [onLoadArchivedTasks]);
|
||||
|
||||
// Tasks are already server-filtered when searchQuery is active (via useTasks hook).
|
||||
// Client-side filtering is removed - tasks prop is used directly.
|
||||
|
||||
@@ -401,7 +401,8 @@ describe("QuickEntryBox", () => {
|
||||
});
|
||||
|
||||
it("prevents default on Enter key (without Shift)", () => {
|
||||
renderQuickEntryBox({});
|
||||
const onCreate = vi.fn(() => new Promise(() => undefined));
|
||||
renderQuickEntryBox({ onCreate });
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Task" } });
|
||||
@@ -497,7 +498,9 @@ describe("QuickEntryBox", () => {
|
||||
fireEvent.keyDown(textarea, { key: "Enter" });
|
||||
|
||||
// Wait a bit to ensure no async call happens
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
});
|
||||
|
||||
expect(props.onCreate).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -509,7 +512,9 @@ describe("QuickEntryBox", () => {
|
||||
fireEvent.change(textarea, { target: { value: " " } });
|
||||
fireEvent.keyDown(textarea, { key: "Enter" });
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
});
|
||||
|
||||
expect(props.onCreate).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -1692,7 +1697,9 @@ describe("QuickEntryBox", () => {
|
||||
it("loading state disables button during refinement", async () => {
|
||||
const { refineText } = await import("../../api");
|
||||
// Slow down the promise to see loading state
|
||||
vi.mocked(refineText).mockImplementation(() => new Promise((resolve) => setTimeout(resolve, 100)));
|
||||
vi.mocked(refineText).mockImplementation(
|
||||
() => new Promise((resolve) => setTimeout(() => resolve("Refined text"), 100)),
|
||||
);
|
||||
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
|
||||
Reference in New Issue
Block a user