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
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2025-02-23 - Concurrent status checks mapping
**Learning:** Performing sequential file and network I/O status checks (like dev server port polling) in a `for...of` loop over multiple items causes severe N+1 bottlenecks.
**Action:** Map items to an array of Promises to initiate them concurrently, and sequentially `await` the promises to preserve correct structural ordering without resorting to `Promise.all()`.
69 changes: 43 additions & 26 deletions src/pages/api/dashboard-projects-health.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import type { APIRoute } from 'astro';
import { desc } from 'drizzle-orm';
import { loadAstroDb } from '../../lib/load-astro-db';
import { resolveProjectPathFromDbProject } from '../../lib/forge-repos';
import { getPrimaryDevServerStatus } from '../../lib/dev-server-status';
import { getWorkSystemStatus } from '../../lib/forge-work-scheduler';
import { fetchZimaOSSessionsPayload, normalizeZimaOSSessions } from '../../lib/forge-gateway';
import type { APIRoute } from "astro";
import { desc } from "drizzle-orm";
import { loadAstroDb } from "../../lib/load-astro-db";
import { resolveProjectPathFromDbProject } from "../../lib/forge-repos";
import { getPrimaryDevServerStatus } from "../../lib/dev-server-status";
import { getWorkSystemStatus } from "../../lib/forge-work-scheduler";
import {
fetchZimaOSSessionsPayload,
normalizeZimaOSSessions,
} from "../../lib/forge-gateway";

type ProjectRow = {
id: number;
Expand All @@ -15,15 +18,17 @@ type ProjectRow = {
updatedAt: Date | null;
};

function mapSessionToRunState(raw: Record<string, unknown>): { running: boolean } {
function mapSessionToRunState(raw: Record<string, unknown>): {
running: boolean;
} {
const mapped = raw as Record<string, unknown>;
const st = String(mapped.status ?? mapped.state ?? '').toLowerCase();
const st = String(mapped.status ?? mapped.state ?? "").toLowerCase();
const active =
st === 'running' ||
st === 'active' ||
st === 'connected' ||
st === 'online' ||
st === 'actif';
st === "running" ||
st === "active" ||
st === "connected" ||
st === "online" ||
st === "actif";
return { running: active };
}

Expand Down Expand Up @@ -69,21 +74,29 @@ export const GET: APIRoute = async ({ locals }) => {

try {
const { db, Project, AgentTask } = await loadAstroDb();
const projects = await db.select().from(Project).orderBy(desc(Project.updatedAt)).limit(12);
const projects = await db
.select()
.from(Project)
.orderBy(desc(Project.updatedAt))
.limit(12);

const tasksAll = await db.select().from(AgentTask).limit(500);

const countForProject = (pid: number | null | undefined) => {
const pend = tasksAll.filter(
(t) =>
t.projectId === pid &&
['pending', 'bug', 'running'].includes(String(t.status || '').toLowerCase()),
["pending", "bug", "running"].includes(
String(t.status || "").toLowerCase(),
),
);
const running = pend.filter((t) => String(t.status || '').toLowerCase() === 'running').length;
const running = pend.filter(
(t) => String(t.status || "").toLowerCase() === "running",
).length;
return { pendingOrRunning: pend.length, running };
};

for (const p of projects as ProjectRow[]) {
const projectPromises = (projects as ProjectRow[]).map(async (p) => {
let dev: {
ok: boolean;
running?: boolean;
Expand All @@ -94,7 +107,7 @@ export const GET: APIRoute = async ({ locals }) => {
hint?: string;
} = {
ok: false,
hint: 'Chemin projet introuvable sur le serveur Forge',
hint: "Chemin projet introuvable sur le serveur Forge",
};

try {
Expand Down Expand Up @@ -123,29 +136,33 @@ export const GET: APIRoute = async ({ locals }) => {
} else {
dev = {
ok: true,
hint: 'Ajoutez `.forge/app-dashboard.json` avec une entrée `servers` pour suivre le dev local.',
hint: "Ajoutez `.forge/app-dashboard.json` avec une entrée `servers` pour suivre le dev local.",
};
}
}
} catch {
dev.hint = 'Erreur lecture disque';
dev.hint = "Erreur lecture disque";
}

payload.projects.push({
return {
id: p.id,
name: p.name,
swarmEnabled: Number(p.swarmEnabled) === 1,
devServer: dev,
tasks: countForProject(p.id),
});
};
});

for (const promise of projectPromises) {
payload.projects.push(await promise);
}

payload.swarm.workScheduler = await getWorkSystemStatus();
} catch (e) {
payload.dbError = e instanceof Error ? e.message : String(e);
return new Response(JSON.stringify(payload), {
status: 200,
headers: { 'Content-Type': 'application/json' },
headers: { "Content-Type": "application/json" },
});
}

Expand All @@ -161,13 +178,13 @@ export const GET: APIRoute = async ({ locals }) => {
if (mapSessionToRunState(s as Record<string, unknown>).running) busy++;
}
payload.swarm.agentsBusy = busy;
payload.swarm.zimaosError = oc.ok ? null : oc.error ?? null;
payload.swarm.zimaosError = oc.ok ? null : (oc.error ?? null);
} catch (e) {
payload.swarm.zimaosError = e instanceof Error ? e.message : String(e);
}

return new Response(JSON.stringify(payload), {
status: 200,
headers: { 'Content-Type': 'application/json' },
headers: { "Content-Type": "application/json" },
});
};