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
Empty file added .jules/bolt.md
Empty file.
210 changes: 142 additions & 68 deletions src/pages/api/work-overview.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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<number, (typeof tasks)[0]> {
const map = new Map<number, (typeof tasks)[0]>();
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]);
Expand All @@ -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();
Expand All @@ -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),
]);
Comment on lines +133 to +148
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

To further optimize the performance of this endpoint, consider selecting only the required columns instead of fetching entire rows. Currently, tables like AgentTask and AgentAppIssue contain potentially large text columns (such as output and detail) that are completely unused in this endpoint. Selecting only the necessary columns will reduce database I/O, network payload size, and memory usage.

    const [projectRows, issues, taskRows, requestRows, depRows] =
      await Promise.all([
        db
          .select({
            id: Project.id,
            name: Project.name,
            swarmEnabled: Project.swarmEnabled,
          })
          .from(Project),
        db
          .select({
            id: AgentAppIssue.id,
            projectId: AgentAppIssue.projectId,
            url: AgentAppIssue.url,
            errorType: AgentAppIssue.errorType,
            title: AgentAppIssue.title,
            status: AgentAppIssue.status,
            reportedByAgentId: AgentAppIssue.reportedByAgentId,
            assigneeAgentId: AgentAppIssue.assigneeAgentId,
            createdAt: AgentAppIssue.createdAt,
            updatedAt: AgentAppIssue.updatedAt,
          })
          .from(AgentAppIssue)
          .orderBy(desc(AgentAppIssue.createdAt))
          .limit(400),
        db
          .select({
            id: AgentTask.id,
            agentId: AgentTask.agentId,
            task: AgentTask.task,
            input: AgentTask.input,
            status: AgentTask.status,
            updatedAt: AgentTask.updatedAt,
          })
          .from(AgentTask)
          .limit(1200),
        db
          .select({
            id: Request.id,
            projectId: Request.projectId,
            title: Request.title,
            content: Request.content,
            status: Request.status,
            priority: Request.priority,
            author: Request.author,
            requestType: Request.requestType,
            assigneeAgentId: Request.assigneeAgentId,
            createdAt: Request.createdAt,
          })
          .from(Request)
          .orderBy(desc(Request.createdAt))
          .limit(200),
        db
          .select({
            id: AgentDependencyRequest.id,
            projectId: AgentDependencyRequest.projectId,
            packageName: AgentDependencyRequest.packageName,
            versionSpec: AgentDependencyRequest.versionSpec,
            status: AgentDependencyRequest.status,
            requestedByAgentId: AgentDependencyRequest.requestedByAgentId,
            createdAt: AgentDependencyRequest.createdAt,
          })
          .from(AgentDependencyRequest)
          .orderBy(desc(AgentDependencyRequest.createdAt))
          .limit(120),
      ]);


const projectRows = await db.select().from(Project);
const projectNames: Record<number, string> = {};
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(),
Expand All @@ -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();
Expand All @@ -164,27 +212,41 @@ export const GET: APIRoute = async () => {
x.setDate(x.getDate() - d);
dayKeys.push(x.toISOString().slice(0, 10));
}
const openedByDay: Record<string, number> = Object.fromEntries(dayKeys.map((k) => [k, 0]));
const resolvedByDay: Record<string, number> = Object.fromEntries(dayKeys.map((k) => [k, 0]));
const openedByDay: Record<string, number> = Object.fromEntries(
dayKeys.map((k) => [k, 0]),
);
const resolvedByDay: Record<string, number> = 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;
}
}

const byProject: Record<string, number> = {};
const byType: Record<string, number> = {};
const bySource: Record<string, number> = { github: 0, detector: 0, agent: 0 };
const bySource: Record<string, number> = {
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;
}
Expand All @@ -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(
Expand All @@ -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,
Expand All @@ -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" },
});
}
};