Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## 2025-01-30 - Replace length checks on full memory fetches with Astro DB aggregations

**Learning:** When retrieving counts for multiple tables or filtered sets (e.g. `Project`, `AgentTask`, `Request`) to display dashboard KPIs, performing full memory queries (using `db.select().from(Table)`) just to read their `.length` property creates significant N+1 and memory overhead, especially for larger data sets. In this codebase's architecture using Drizzle ORM over Astro DB, you can offload these aggregations directly to the database.

**Action:** Replaced full row fetches mapping to `.length` arrays in `src/pages/api/dashboard-kpis.ts` with direct Drizzle ORM `db.select({ count: count() })` along with appropriate `where` conditions (`eq`, `gte`, `inArray`). This performs the counting fully on the SQLite layer, dramatically reducing memory payload and computation time.
37 changes: 14 additions & 23 deletions src/pages/api/dashboard-kpis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,6 @@ function countRunningSessions(sessions: unknown[]): number {
}).length;
}

function isOpenQueueStatus(st: string): boolean {
const s = String(st).toLowerCase();
return s === 'open' || s === 'in_progress';
}

export const GET: APIRoute = async () => {
const base = {
projectCount: 0,
Expand All @@ -42,30 +37,26 @@ export const GET: APIRoute = async () => {
};

try {
const { db, Project, AgentTask, Request, AgentAppIssue, AgentDependencyRequest, eq } =
const { db, Project, AgentTask, Request, AgentAppIssue, AgentDependencyRequest, eq, count, inArray, gte } =
await loadAstroDb();
const today = new Date();
today.setHours(0, 0, 0, 0);

const [projects, tasksAll, openRequests, issuesAll, depsAll] = await Promise.all([
db.select().from(Project),
db.select().from(AgentTask),
db.select().from(Request).where(eq(Request.status, 'pending')),
db.select().from(AgentAppIssue),
db.select().from(AgentDependencyRequest),
const [projectsRes, tasksTotalRes, tasksTodayRes, openRequestsRes, openIssuesRes, openDepsRes] = await Promise.all([
db.select({ count: count() }).from(Project),
db.select({ count: count() }).from(AgentTask),
db.select({ count: count() }).from(AgentTask).where(gte(AgentTask.createdAt, today)),
db.select({ count: count() }).from(Request).where(eq(Request.status, 'pending')),
db.select({ count: count() }).from(AgentAppIssue).where(inArray(AgentAppIssue.status, ['open', 'in_progress'])),
db.select({ count: count() }).from(AgentDependencyRequest).where(inArray(AgentDependencyRequest.status, ['open', 'in_progress'])),
Comment on lines +50 to +51

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The original implementation used isOpenQueueStatus which performed a case-insensitive check (.toLowerCase()) on the status field. By switching to a direct SQL inArray comparison, the query is now case-sensitive in SQLite.

If there is any chance of mixed-case status values being inserted into the database (e.g., by external agents or legacy data), they will no longer be counted.

To prevent this regression, it is highly recommended to ensure strict lowercase validation at the database insertion/ingestion layer (e.g., via Zod schemas or database defaults) so that the database remains clean and the queries can run efficiently without needing case-insensitive SQL functions.

]);

const tasksTodayCount = tasksAll.filter((t) => {
const d = t.createdAt instanceof Date ? t.createdAt : new Date(t.createdAt as Date);
return d >= today;
}).length;

base.projectCount = projects.length;
base.tasksTotal = tasksAll.length;
base.tasksToday = tasksTodayCount;
base.openRequests = openRequests.length;
base.openAppIssues = issuesAll.filter((r) => isOpenQueueStatus(String(r.status))).length;
base.openDependencyRequests = depsAll.filter((r) => isOpenQueueStatus(String(r.status))).length;
base.projectCount = Number(projectsRes[0]?.count ?? 0);
base.tasksTotal = Number(tasksTotalRes[0]?.count ?? 0);
base.tasksToday = Number(tasksTodayRes[0]?.count ?? 0);
base.openRequests = Number(openRequestsRes[0]?.count ?? 0);
base.openAppIssues = Number(openIssuesRes[0]?.count ?? 0);
base.openDependencyRequests = Number(openDepsRes[0]?.count ?? 0);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
base.dbError = msg;
Expand Down