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:
@@ -1322,6 +1322,42 @@ describe("TaskStore", () => {
|
||||
expect(paged).toHaveLength(1);
|
||||
expect(paged[0].id).toBe("FN-002");
|
||||
});
|
||||
|
||||
it("slim mode returns metadata but drops heavy fields (log/comments/steps)", async () => {
|
||||
const task = await store.createTask({ description: "Slim test" });
|
||||
await store.logEntry(task.id, "heavy log entry that should not appear in slim list");
|
||||
|
||||
const fullList = await store.listTasks();
|
||||
const slimList = await store.listTasks({ slim: true });
|
||||
|
||||
const full = fullList.find((t) => t.id === task.id)!;
|
||||
const slim = slimList.find((t) => t.id === task.id)!;
|
||||
|
||||
expect(full.log.length).toBeGreaterThan(0);
|
||||
expect(slim.id).toBe(task.id);
|
||||
expect(slim.description).toBe("Slim test");
|
||||
expect(slim.column).toBe(full.column);
|
||||
expect(slim.log).toEqual([]);
|
||||
expect(slim.steps).toEqual([]);
|
||||
expect(slim.comments).toBeUndefined();
|
||||
});
|
||||
|
||||
it("includeArchived=false excludes archived tasks; default includes them", async () => {
|
||||
const keep = await store.createTask({ description: "Stays visible" });
|
||||
const toArchive = await store.createTask({ description: "Will be archived" });
|
||||
await store.moveTask(toArchive.id, "todo");
|
||||
await store.moveTask(toArchive.id, "in-progress");
|
||||
await store.moveTask(toArchive.id, "in-review");
|
||||
await store.moveTask(toArchive.id, "done");
|
||||
await store.archiveTask(toArchive.id);
|
||||
|
||||
const withArchived = await store.listTasks();
|
||||
const withoutArchived = await store.listTasks({ includeArchived: false });
|
||||
|
||||
expect(withArchived.map((t) => t.id)).toEqual(expect.arrayContaining([keep.id, toArchive.id]));
|
||||
expect(withoutArchived.map((t) => t.id)).toContain(keep.id);
|
||||
expect(withoutArchived.map((t) => t.id)).not.toContain(toArchive.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe("SQLite-first reads when task blobs are missing", () => {
|
||||
|
||||
@@ -1307,8 +1307,39 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
return { ...task, prompt };
|
||||
}
|
||||
|
||||
async listTasks(options?: { limit?: number; offset?: number }): Promise<Task[]> {
|
||||
const rows = this.db.prepare('SELECT * FROM tasks ORDER BY createdAt ASC').all();
|
||||
async listTasks(options?: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
/** When false, exclude tasks in the `archived` column. Default: true (backward compatible). */
|
||||
includeArchived?: boolean;
|
||||
/** When true, omit heavy fields (log, comments, steps, workflowStepResults, steeringComments)
|
||||
* from each row to make list responses cheap for board-style consumers. Detail fields default
|
||||
* to empty arrays in the returned Task objects; use `getTask(id)` to load full data. */
|
||||
slim?: boolean;
|
||||
}): Promise<Task[]> {
|
||||
const includeArchived = options?.includeArchived ?? true;
|
||||
const slim = options?.slim ?? false;
|
||||
|
||||
const slimColumns = `
|
||||
id, title, description, "column", status, size, reviewLevel, currentStep,
|
||||
worktree, blockedBy, paused, baseBranch, branch, baseCommitSha,
|
||||
modelPresetId, modelProvider, modelId,
|
||||
validatorModelProvider, validatorModelId,
|
||||
planningModelProvider, planningModelId,
|
||||
mergeRetries, stuckKillCount, recoveryRetryCount, nextRecoveryAt,
|
||||
error, summary, thinkingLevel,
|
||||
createdAt, updatedAt, columnMovedAt,
|
||||
dependencies,
|
||||
attachments, prInfo, issueInfo, mergeDetails,
|
||||
breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles,
|
||||
missionId, sliceId, assignedAgentId, assigneeUserId,
|
||||
checkedOutBy, checkedOutAt
|
||||
`;
|
||||
const selectClause = slim ? slimColumns : '*';
|
||||
const whereClause = includeArchived ? '' : ` WHERE "column" != 'archived'`;
|
||||
const sql = `SELECT ${selectClause} FROM tasks${whereClause} ORDER BY createdAt ASC`;
|
||||
|
||||
const rows = this.db.prepare(sql).all();
|
||||
const tasks = (rows as any[]).map((row) => this.rowToTask(row));
|
||||
|
||||
// Sort by createdAt, then by numeric ID suffix for tie-breaking
|
||||
|
||||
Reference in New Issue
Block a user