diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 00000000..e69de29b diff --git a/src/pages/api/work-overview.ts b/src/pages/api/work-overview.ts index 4916a44d..9cf9cded 100644 --- a/src/pages/api/work-overview.ts +++ b/src/pages/api/work-overview.ts @@ -1,5 +1,5 @@ -import type { APIRoute } from 'astro'; -import { loadAstroDb } from '../../lib/load-astro-db'; +import type { APIRoute } from "astro"; +import { loadAstroDb } from "../../lib/load-astro-db"; export type WorkOverviewIssue = { id: number; @@ -14,8 +14,8 @@ export type WorkOverviewIssue = { createdAt: string; updatedAt: string; /** github | detector | agent */ - source: 'github' | 'detector' | 'agent'; - kanbanColumn: 'detected' | 'dispatched' | 'running' | 'resolved'; + source: "github" | "detector" | "agent"; + kanbanColumn: "detected" | "dispatched" | "running" | "resolved"; linkedTask: { id: number; agentId: string; @@ -26,29 +26,38 @@ export type WorkOverviewIssue = { sparkline: number[]; }; -function classifySource(reportedBy: string): WorkOverviewIssue['source'] { - const r = String(reportedBy || '').trim(); - if (r === 'SYSTEM_GITHUB') return 'github'; - if (r === 'BUG_DETECTOR') return 'detector'; - return 'agent'; +function classifySource(reportedBy: string): WorkOverviewIssue["source"] { + const r = String(reportedBy || "").trim(); + if (r === "SYSTEM_GITHUB") return "github"; + if (r === "BUG_DETECTOR") return "detector"; + return "agent"; } function rankTaskStatus(s: string): number { - const x = String(s || '').toLowerCase(); - if (x === 'running') return 4; - if (x === 'pending' || x === 'bug') return 3; - if (x === 'completed' || x === 'done') return 2; + const x = String(s || "").toLowerCase(); + if (x === "running") return 4; + if (x === "pending" || x === "bug") return 3; + if (x === "completed" || x === "done") return 2; return 1; } function buildIssueTaskMap( - tasks: { id: number; agentId: string; task: string | null; input: string | null; status: string; updatedAt: Date | string }[], + tasks: { + id: number; + agentId: string; + task: string | null; + input: string | null; + status: string; + updatedAt: Date | string; + }[], ): Map { const map = new Map(); const time = (t: (typeof tasks)[0]) => - t.updatedAt instanceof Date ? t.updatedAt.getTime() : new Date(String(t.updatedAt || 0)).getTime(); + t.updatedAt instanceof Date + ? t.updatedAt.getTime() + : new Date(String(t.updatedAt || 0)).getTime(); for (const t of tasks) { - const blob = `${t.task ?? ''}\n${t.input ?? ''}`; + const blob = `${t.task ?? ""}\n${t.input ?? ""}`; const m = blob.match(/AppIssue\s*#(\d+)/i); if (!m) continue; const issueId = Number(m[1]); @@ -66,23 +75,27 @@ function buildIssueTaskMap( function kanbanForIssue( issue: { status: string; id: number }, task: { status: string } | undefined, -): WorkOverviewIssue['kanbanColumn'] { - const ist = String(issue.status || '').toLowerCase(); - if (ist === 'resolved' || ist === 'wont_fix') return 'resolved'; - const ts = task ? String(task.status || '').toLowerCase() : ''; - if (ts === 'running') return 'running'; - if (ist === 'in_progress' || ts === 'pending' || ts === 'bug') return 'dispatched'; - return 'detected'; +): WorkOverviewIssue["kanbanColumn"] { + const ist = String(issue.status || "").toLowerCase(); + if (ist === "resolved" || ist === "wont_fix") return "resolved"; + const ts = task ? String(task.status || "").toLowerCase() : ""; + if (ts === "running") return "running"; + if (ist === "in_progress" || ts === "pending" || ts === "bug") + return "dispatched"; + return "detected"; } function isAgentLikeAuthor(author: string | null | undefined): boolean { - const a = String(author || '').trim(); + const a = String(author || "").trim(); if (!a) return false; if (/^(utilisateur|Mathieu|HUMAIN|dashboard)$/i.test(a)) return false; return /^[A-Z][A-Z0-9_]*(__[A-Z0-9_]+)?$/.test(a); } -function sparklineForIssue(createdAt: Date | string, updatedAt: Date | string): number[] { +function sparklineForIssue( + createdAt: Date | string, + updatedAt: Date | string, +): number[] { const c = createdAt instanceof Date ? createdAt : new Date(String(createdAt)); const u = updatedAt instanceof Date ? updatedAt : new Date(String(updatedAt)); const now = new Date(); @@ -106,33 +119,63 @@ function sparklineForIssue(createdAt: Date | string, updatedAt: Date | string): /** GET — agrégats carnet : projets, anomalies enrichies, séries 30j, idées agents */ export const GET: APIRoute = async () => { try { - const { db, AgentAppIssue, AgentTask, AgentDependencyRequest, Request, Project, desc } = await loadAstroDb(); + const { + db, + AgentAppIssue, + AgentTask, + AgentDependencyRequest, + Request, + Project, + desc, + } = await loadAstroDb(); + + // Parallelize independent database queries to reduce total wait time + const [projectRows, issues, taskRows, requestRows, depRows] = + await Promise.all([ + db.select().from(Project), + db + .select() + .from(AgentAppIssue) + .orderBy(desc(AgentAppIssue.createdAt)) + .limit(400), + db.select().from(AgentTask).limit(1200), + db.select().from(Request).orderBy(desc(Request.createdAt)).limit(200), + db + .select() + .from(AgentDependencyRequest) + .orderBy(desc(AgentDependencyRequest.createdAt)) + .limit(120), + ]); - const projectRows = await db.select().from(Project); const projectNames: Record = {}; for (const p of projectRows) { - projectNames[p.id] = String(p.name || ''); + projectNames[p.id] = String(p.name || ""); } - const issues = await db.select().from(AgentAppIssue).orderBy(desc(AgentAppIssue.createdAt)).limit(400); - const taskRows = await db.select().from(AgentTask).limit(1200); const issueTaskMap = buildIssueTaskMap(taskRows); const enriched: WorkOverviewIssue[] = issues.map((i) => { const ti = issueTaskMap.get(i.id); - const src = classifySource(String(i.reportedByAgentId || '')); + const src = classifySource(String(i.reportedByAgentId || "")); const col = kanbanForIssue(i, ti); - const cr = i.createdAt instanceof Date ? i.createdAt : new Date(String(i.createdAt)); - const up = i.updatedAt instanceof Date ? i.updatedAt : new Date(String(i.updatedAt)); + const cr = + i.createdAt instanceof Date + ? i.createdAt + : new Date(String(i.createdAt)); + const up = + i.updatedAt instanceof Date + ? i.updatedAt + : new Date(String(i.updatedAt)); return { id: i.id, projectId: i.projectId ?? null, - projectName: i.projectId != null ? projectNames[i.projectId] ?? '—' : '—', - title: String(i.title || ''), - url: String(i.url || ''), - errorType: String(i.errorType || ''), - status: String(i.status || ''), - reportedByAgentId: String(i.reportedByAgentId || ''), + projectName: + i.projectId != null ? (projectNames[i.projectId] ?? "—") : "—", + title: String(i.title || ""), + url: String(i.url || ""), + errorType: String(i.errorType || ""), + status: String(i.status || ""), + reportedByAgentId: String(i.reportedByAgentId || ""), assigneeAgentId: i.assigneeAgentId ? String(i.assigneeAgentId) : null, createdAt: cr.toISOString(), updatedAt: up.toISOString(), @@ -141,20 +184,25 @@ export const GET: APIRoute = async () => { linkedTask: ti ? { id: ti.id, - agentId: String(ti.agentId || ''), - status: String(ti.status || ''), - updatedAt: ti.updatedAt instanceof Date ? ti.updatedAt.toISOString() : String(ti.updatedAt), + agentId: String(ti.agentId || ""), + status: String(ti.status || ""), + updatedAt: + ti.updatedAt instanceof Date + ? ti.updatedAt.toISOString() + : String(ti.updatedAt), } : null, sparkline: sparklineForIssue(cr, up), }; }); - const requestRows = await db.select().from(Request).orderBy(desc(Request.createdAt)).limit(200); const agentIdeas = requestRows.filter((r) => { - const rt = String(r.requestType || ''); - if (rt === 'Amélioration') return true; - return isAgentLikeAuthor(r.author) && (rt === 'Fonctionnalite' || rt === 'Correction'); + const rt = String(r.requestType || ""); + if (rt === "Amélioration") return true; + return ( + isAgentLikeAuthor(r.author) && + (rt === "Fonctionnalite" || rt === "Correction") + ); }); const now = new Date(); @@ -164,15 +212,25 @@ export const GET: APIRoute = async () => { x.setDate(x.getDate() - d); dayKeys.push(x.toISOString().slice(0, 10)); } - const openedByDay: Record = Object.fromEntries(dayKeys.map((k) => [k, 0])); - const resolvedByDay: Record = Object.fromEntries(dayKeys.map((k) => [k, 0])); + const openedByDay: Record = Object.fromEntries( + dayKeys.map((k) => [k, 0]), + ); + const resolvedByDay: Record = Object.fromEntries( + dayKeys.map((k) => [k, 0]), + ); for (const i of issues) { - const c = i.createdAt instanceof Date ? i.createdAt : new Date(String(i.createdAt)); + const c = + i.createdAt instanceof Date + ? i.createdAt + : new Date(String(i.createdAt)); const k = c.toISOString().slice(0, 10); if (openedByDay[k] !== undefined) openedByDay[k] += 1; - const st = String(i.status || '').toLowerCase(); - if (st === 'resolved' || st === 'wont_fix') { - const u = i.updatedAt instanceof Date ? i.updatedAt : new Date(String(i.updatedAt)); + const st = String(i.status || "").toLowerCase(); + if (st === "resolved" || st === "wont_fix") { + const u = + i.updatedAt instanceof Date + ? i.updatedAt + : new Date(String(i.updatedAt)); const ku = u.toISOString().slice(0, 10); if (resolvedByDay[ku] !== undefined) resolvedByDay[ku] += 1; } @@ -180,11 +238,15 @@ export const GET: APIRoute = async () => { const byProject: Record = {}; const byType: Record = {}; - const bySource: Record = { github: 0, detector: 0, agent: 0 }; + const bySource: Record = { + github: 0, + detector: 0, + agent: 0, + }; for (const e of enriched) { - const pn = e.projectName || '—'; + const pn = e.projectName || "—"; byProject[pn] = (byProject[pn] || 0) + 1; - const et = e.errorType || 'other'; + const et = e.errorType || "other"; byType[et] = (byType[et] || 0) + 1; bySource[e.source] = (bySource[e.source] || 0) + 1; } @@ -195,32 +257,39 @@ export const GET: APIRoute = async () => { resolved: resolvedByDay[k] ?? 0, })); - const proposalsPending = requestRows.filter((r) => String(r.status) === 'pending').length; - - const depRows = await db.select().from(AgentDependencyRequest).orderBy(desc(AgentDependencyRequest.createdAt)).limit(120); + const proposalsPending = requestRows.filter( + (r) => String(r.status) === "pending", + ).length; const requestsPreview = requestRows.map((r) => ({ id: r.id, projectId: r.projectId, - projectName: projectNames[r.projectId] ?? '—', + projectName: projectNames[r.projectId] ?? "—", title: r.title, content: r.content, status: r.status, priority: r.priority, author: r.author, requestType: r.requestType, - createdAt: r.createdAt instanceof Date ? r.createdAt.toISOString() : String(r.createdAt), + createdAt: + r.createdAt instanceof Date + ? r.createdAt.toISOString() + : String(r.createdAt), })); const depsPreview = depRows.map((d) => ({ id: d.id, projectId: d.projectId, - projectName: d.projectId != null ? projectNames[d.projectId] ?? '—' : '—', + projectName: + d.projectId != null ? (projectNames[d.projectId] ?? "—") : "—", packageName: d.packageName, versionSpec: d.versionSpec, status: d.status, requestedByAgentId: d.requestedByAgentId, - createdAt: d.createdAt instanceof Date ? d.createdAt.toISOString() : String(d.createdAt), + createdAt: + d.createdAt instanceof Date + ? d.createdAt.toISOString() + : String(d.createdAt), })); return new Response( @@ -244,7 +313,10 @@ export const GET: APIRoute = async () => { author: r.author, requestType: r.requestType, assigneeAgentId: r.assigneeAgentId, - createdAt: r.createdAt instanceof Date ? r.createdAt.toISOString() : String(r.createdAt), + createdAt: + r.createdAt instanceof Date + ? r.createdAt.toISOString() + : String(r.createdAt), })), charts: { byProject, @@ -255,22 +327,24 @@ export const GET: APIRoute = async () => { counts: { issues: issues.length, openIssues: issues.filter((i) => { - const s = String(i.status || '').toLowerCase(); - return s === 'open' || s === 'in_progress'; + const s = String(i.status || "").toLowerCase(); + return s === "open" || s === "in_progress"; }).length, requests: requestRows.length, deps: depRows.length, proposalsPending, }, - swarmProjectCount: projectRows.filter((p) => Number(p.swarmEnabled) === 1).length, + swarmProjectCount: projectRows.filter( + (p) => Number(p.swarmEnabled) === 1, + ).length, }), - { status: 200, headers: { 'Content-Type': 'application/json' } }, + { status: 200, headers: { "Content-Type": "application/json" } }, ); } catch (e: unknown) { const msg = e instanceof Error ? e.message : String(e); return new Response(JSON.stringify({ ok: false, error: msg }), { status: 500, - headers: { 'Content-Type': 'application/json' }, + headers: { "Content-Type": "application/json" }, }); } };