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:
gsxdsm
2026-04-10 20:16:20 -07:00
parent c9a1e94bbe
commit 5984584d22
11 changed files with 333 additions and 82 deletions

View File

@@ -46,6 +46,11 @@ export function useTasks(options?: UseTasksOptions) {
const searchQuery = options?.searchQuery;
const [tasks, setTasks] = useState<Task[]>([]);
const [connectionNonce, setConnectionNonce] = useState(0);
// Once the user expands the archived column, we keep including archived tasks
// in subsequent refreshes for the lifetime of this hook instance.
const [includeArchived, setIncludeArchived] = useState(false);
const includeArchivedRef = useRef(includeArchived);
includeArchivedRef.current = includeArchived;
const tasksRef = useRef(tasks);
const fetchVersionRef = useRef(0);
const lastVisibilityRefreshRef = useRef<number>(0);
@@ -56,12 +61,13 @@ export function useTasks(options?: UseTasksOptions) {
const VISIBILITY_REFRESH_DEBOUNCE_MS = 1000;
const refreshTasks = useCallback(async (options?: { clearOnError?: boolean; searchQueryOverride?: string }) => {
const refreshTasks = useCallback(async (options?: { clearOnError?: boolean; searchQueryOverride?: string; includeArchivedOverride?: boolean }) => {
const requestVersion = ++fetchVersionRef.current;
const query = options?.searchQueryOverride ?? searchQuery;
const wantArchived = options?.includeArchivedOverride ?? includeArchivedRef.current;
try {
const fetchedTasks = await api.fetchTasks(undefined, undefined, projectId, query);
const fetchedTasks = await api.fetchTasks(undefined, undefined, projectId, query, wantArchived);
if (fetchVersionRef.current !== requestVersion) {
return;
}
@@ -79,6 +85,14 @@ export function useTasks(options?: UseTasksOptions) {
}, [projectId, searchQuery]);
refreshTasksRef.current = refreshTasks;
/** Lazy-load archived tasks. Called by the Board when the archived column is first expanded. */
const loadArchivedTasks = useCallback(async () => {
if (includeArchivedRef.current) return;
setIncludeArchived(true);
includeArchivedRef.current = true;
await refreshTasksRef.current({ includeArchivedOverride: true });
}, []);
// Debounced search effect - separate from refreshTasks to avoid dependency cycle
useEffect(() => {
if (searchQuery === undefined) return;
@@ -380,5 +394,5 @@ export function useTasks(options?: UseTasksOptions) {
return normalized;
}, [projectId]);
return { tasks, createTask, moveTask, deleteTask, mergeTask, retryTask, duplicateTask, updateTask, archiveTask, unarchiveTask, archiveAllDone };
return { tasks, createTask, moveTask, deleteTask, mergeTask, retryTask, duplicateTask, updateTask, archiveTask, unarchiveTask, archiveAllDone, loadArchivedTasks, includeArchived };
}