diff --git a/src/main/api-server/routes-work-graph.ts b/src/main/api-server/routes-work-graph.ts index 5fa31d80..05e7bd37 100644 --- a/src/main/api-server/routes-work-graph.ts +++ b/src/main/api-server/routes-work-graph.ts @@ -3,32 +3,6 @@ import { goalInputSchema, projectInputSchema } from "@shared/contracts"; import { canManageGoal, canManageProject } from "../agent-permissions"; import { requireParam, type AgentApiServer } from "./server-core"; -type ProjectRow = { - id: string; companyId: string; goalId: string | null; name: string; - description: string; leadAgentId: string | null; status: string; - targetDate: string | null; createdAt: string; updatedAt: string; -}; - -type GoalRow = { - id: string; companyId: string; title: string; description: string; - parentId: string | null; ownerAgentId: string | null; status: string; - createdAt: string; updatedAt: string; -}; - -type SprintRow = { - id: string; companyId: string; name: string; goal: string; - startDate: string; endDate: string; status: string; - retrospectiveNotes: string | null; velocityPoints: number; - completedPoints: number; createdAt: string; updatedAt: string; -}; - -type AgentStatusRow = { id: string; status: string }; -type TaskStatusRow = { id: string; status: string }; -type ActivityRow = { - id: string; companyId: string; actor: string; action: string; - entityType: string; entityId: string; detail: string | null; createdAt: string; -}; - export function registerWorkGraphRoutes(server: AgentApiServer) { const db = server.db; const onDomainChanged = server.onDomainChanged; @@ -104,11 +78,7 @@ export function registerWorkGraphRoutes(server: AgentApiServer) { return; } - const projectRows = db.queryAll( - "select id, company_id as companyId, goal_id as goalId, name, description, lead_agent_id as leadAgentId, status, target_date as targetDate, created_at as createdAt, updated_at as updatedAt from projects where id = ? and company_id = ? limit 1", - projectId, token.companyId, - ); - const existing = projectRows[0] ?? null; + const existing = db.getProjectForUpdate(projectId, token.companyId); if (!existing) { server.json(res, 404, { error: "Project not found" }); return; @@ -195,11 +165,7 @@ export function registerWorkGraphRoutes(server: AgentApiServer) { return; } - const goalRows = db.queryAll( - "select id, company_id as companyId, title, description, parent_id as parentId, owner_agent_id as ownerAgentId, status, created_at as createdAt, updated_at as updatedAt from goals where id = ? and company_id = ? limit 1", - goalId, token.companyId, - ); - const existing = goalRows[0] ?? null; + const existing = db.getGoalForUpdate(goalId, token.companyId); if (!existing) { server.json(res, 404, { error: "Goal not found" }); return; @@ -231,10 +197,7 @@ export function registerWorkGraphRoutes(server: AgentApiServer) { server.json(res, 403, { error: "Company mismatch" }); return; } - const goals = db.queryAll( - "select id, company_id as companyId, title, description, parent_id as parentId, owner_agent_id as ownerAgentId, status, created_at as createdAt, updated_at as updatedAt from goals where company_id = ? order by created_at asc", - params.companyId, - ); + const goals = db.listGoals(params.companyId); server.json(res, 200, goals); }); @@ -243,10 +206,7 @@ export function registerWorkGraphRoutes(server: AgentApiServer) { server.json(res, 403, { error: "Company mismatch" }); return; } - const projects = db.queryAll( - "select id, company_id as companyId, goal_id as goalId, name, description, lead_agent_id as leadAgentId, status, target_date as targetDate, created_at as createdAt, updated_at as updatedAt from projects where company_id = ? order by created_at desc", - params.companyId, - ); + const projects = db.listProjects(params.companyId); server.json(res, 200, projects); }); @@ -256,10 +216,7 @@ export function registerWorkGraphRoutes(server: AgentApiServer) { server.json(res, 403, { error: "Company mismatch" }); return; } - const sprints = db.queryAll( - "select id, company_id as companyId, name, goal, start_date as startDate, end_date as endDate, status, retrospective_notes as retrospectiveNotes, velocity_points as velocityPoints, completed_points as completedPoints, created_at as createdAt, updated_at as updatedAt from sprints where company_id = ? order by start_date desc", - companyId, - ); + const sprints = db.listSprints(companyId); server.json(res, 200, sprints); }); @@ -295,18 +252,7 @@ export function registerWorkGraphRoutes(server: AgentApiServer) { server.json(res, 403, { error: "Company mismatch" }); return; } - const agents = db.queryAll( - "select id, status from agents where company_id = ?", - companyId, - ); - const tasks = db.queryAll( - "select id, status from tasks where company_id = ?", - companyId, - ); - const recentActivity = db.queryAll( - "select id, company_id as companyId, actor, action, entity_type as entityType, entity_id as entityId, detail, created_at as createdAt from activity where company_id = ? order by created_at desc limit 20", - companyId, - ); + const { agents, tasks, recentActivity } = db.getDashboardSnapshot(companyId); const tasksByStatus: Record = {}; for (const t of tasks) { diff --git a/src/main/database-base.ts b/src/main/database-base.ts index 60531dc4..f6e6e7e4 100644 --- a/src/main/database-base.ts +++ b/src/main/database-base.ts @@ -190,6 +190,7 @@ export class AppDatabaseBase { getInbox(companyId: string) { return this.company.getInbox(companyId); } getCompanyMetrics(companyId: string, days: number = 30) { return this.company.getCompanyMetrics(companyId, days); } getStandupReport(companyId: string) { return this.company.getStandupReport(companyId); } + getDashboardSnapshot(companyId: string) { return this.company.getDashboardSnapshot(companyId); } // ─── Connectors ──────────────────────────────────────────────────────────── diff --git a/src/main/database.ts b/src/main/database.ts index 5de9c6ce..a9b95520 100644 --- a/src/main/database.ts +++ b/src/main/database.ts @@ -65,7 +65,11 @@ export class AppDatabase extends AppDatabaseBase { // ─── Entity CRUD ─────────────────────────────────────────────────────────── saveGoal(input: { id?: string | null; companyId: string; title: string; description: string; parentId?: string | null; ownerAgentId?: string | null; status: string }) { return this.entityCrud.saveGoal(input); } + listGoals(companyId: string) { return this.entityCrud.listGoals(companyId); } + getGoalForUpdate(goalId: string, companyId: string) { return this.entityCrud.getGoalForUpdate(goalId, companyId); } saveProject(input: { id?: string | null; companyId: string; goalId?: string | null; name: string; description: string; leadAgentId?: string | null; targetDate?: string | null; status: string }) { return this.entityCrud.saveProject(input); } + listProjects(companyId: string) { return this.entityCrud.listProjects(companyId); } + getProjectForUpdate(projectId: string, companyId: string) { return this.entityCrud.getProjectForUpdate(projectId, companyId); } saveSecret(input: { id?: string | null; companyId: string; name: string; description: string; value: string }) { return this.entityCrud.saveSecret(input, this.vault); } resolveSecretEnv(bindingText: string, companyId: string) { return this.entityCrud.resolveSecretEnv(bindingText, companyId, this.vault, listInvalidEnvBindingKeys, parseEnvBindingText); } deleteGoal(id: string, companyId: string) { this.entityCrud.deleteGoal(id, companyId); } @@ -118,6 +122,7 @@ export class AppDatabase extends AppDatabaseBase { listKnowledgeEntries(companyId: string) { return this.knowledgeMod.listKnowledgeEntries(companyId); } saveSprint(input: { id?: string | null; companyId: string; name: string; goal?: string; startDate: string; endDate: string; status?: string; retrospectiveNotes?: string; velocityPoints?: number; completedPoints?: number }) { return this.knowledgeMod.saveSprint(input); } deleteSprint(id: string, companyId: string) { this.knowledgeMod.deleteSprint(id, companyId); } + listSprints(companyId: string) { return this.knowledgeMod.listSprints(companyId); } // ─── Automation ──────────────────────────────────────────────────────────── diff --git a/src/main/database/company-settings.ts b/src/main/database/company-settings.ts index cd8d0b7d..dd7e0f3f 100644 --- a/src/main/database/company-settings.ts +++ b/src/main/database/company-settings.ts @@ -21,6 +21,44 @@ export function buildCompanySettingsMethods( }, ) { return { + /** + * Returns the raw inputs for the GET /api/companies/:id/dashboard + * endpoint: agent statuses, task statuses, and the last 20 activity rows. + * Route code aggregates counts by status on top of this raw data. + */ + getDashboardSnapshot(companyId: string): { + agents: Array<{ id: string; status: string }>; + tasks: Array<{ id: string; status: string }>; + recentActivity: Array<{ + id: string; + companyId: string; + actor: string; + action: string; + entityType: string; + entityId: string; + detail: string | null; + createdAt: string; + }>; + } { + return { + agents: readAll<{ id: string; status: string }>( + "select id, status from agents where company_id = ?", + companyId, + ), + tasks: readAll<{ id: string; status: string }>( + "select id, status from tasks where company_id = ?", + companyId, + ), + recentActivity: readAll<{ + id: string; companyId: string; actor: string; action: string; + entityType: string; entityId: string; detail: string | null; createdAt: string; + }>( + "select id, company_id as companyId, actor, action, entity_type as entityType, entity_id as entityId, detail, created_at as createdAt from activity where company_id = ? order by created_at desc limit 20", + companyId, + ), + }; + }, + getCompanyMetrics(companyId: string, days: number = 30): CompanyMetrics { const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString(); const db = getDb(); diff --git a/src/main/database/entity-crud.ts b/src/main/database/entity-crud.ts index 455e48d7..89ed5666 100644 --- a/src/main/database/entity-crud.ts +++ b/src/main/database/entity-crud.ts @@ -3,6 +3,40 @@ import type { AgentRecord, ConnectorId, TaskRecord } from "@shared/types"; import type { ReadAll, Transaction } from "./helpers"; import { ensureId, normalizeOptionalId, nowIso } from "./helpers"; +/** + * Goal row shape returned by listGoals / getGoalForUpdate. Mirrors the + * historical inline SQL projection in routes-work-graph.ts so HTTP + * responses stay byte-stable. + */ +export type GoalListRow = { + id: string; + companyId: string; + title: string; + description: string; + parentId: string | null; + ownerAgentId: string | null; + status: string; + createdAt: string; + updatedAt: string; +}; + +/** + * Project row shape returned by listProjects / getProjectForUpdate. + * Mirrors the historical inline SQL projection in routes-work-graph.ts. + */ +export type ProjectListRow = { + id: string; + companyId: string; + goalId: string | null; + name: string; + description: string; + leadAgentId: string | null; + status: string; + targetDate: string | null; + createdAt: string; + updatedAt: string; +}; + export function buildEntityCrudMethods( getDb: () => DatabaseSync, readAll: ReadAll, @@ -37,6 +71,53 @@ export function buildEntityCrudMethods( return row; }, + /** + * Returns all goals for a company. Used by GET /api/companies/:id/goals. + */ + listGoals(companyId: string): GoalListRow[] { + return readAll( + "select id, company_id as companyId, title, description, parent_id as parentId, owner_agent_id as ownerAgentId, status, created_at as createdAt, updated_at as updatedAt from goals where company_id = ? order by created_at asc", + companyId, + ); + }, + + /** + * Returns the goal lookup row used by PATCH /api/goals/:goalId. + * Returns null when the goal is missing or belongs to another company. + */ + getGoalForUpdate(goalId: string, companyId: string): GoalListRow | null { + const row = getDb() + .prepare( + "select id, company_id as companyId, title, description, parent_id as parentId, owner_agent_id as ownerAgentId, status, created_at as createdAt, updated_at as updatedAt from goals where id = ? and company_id = ? limit 1", + ) + .get(goalId, companyId) as GoalListRow | undefined; + return row ?? null; + }, + + /** + * Returns all projects for a company. Used by + * GET /api/companies/:id/projects. + */ + listProjects(companyId: string): ProjectListRow[] { + return readAll( + "select id, company_id as companyId, goal_id as goalId, name, description, lead_agent_id as leadAgentId, status, target_date as targetDate, created_at as createdAt, updated_at as updatedAt from projects where company_id = ? order by created_at desc", + companyId, + ); + }, + + /** + * Returns the project lookup row used by PATCH /api/projects/:projectId. + * Returns null when the project is missing or belongs to another company. + */ + getProjectForUpdate(projectId: string, companyId: string): ProjectListRow | null { + const row = getDb() + .prepare( + "select id, company_id as companyId, goal_id as goalId, name, description, lead_agent_id as leadAgentId, status, target_date as targetDate, created_at as createdAt, updated_at as updatedAt from projects where id = ? and company_id = ? limit 1", + ) + .get(projectId, companyId) as ProjectListRow | undefined; + return row ?? null; + }, + saveGoal(input: { id?: string | null; companyId: string; title: string; description: string; parentId?: string | null; ownerAgentId?: string | null; status: string }) { const id = ensureId(input.id ?? null); const ts = nowIso(); diff --git a/src/main/database/knowledge.ts b/src/main/database/knowledge.ts index f7bd4259..a3aa556c 100644 --- a/src/main/database/knowledge.ts +++ b/src/main/database/knowledge.ts @@ -28,6 +28,26 @@ export type MeetingListRow = { const MEETING_LIST_COLUMNS = "select id, company_id as companyId, type, title, organizer_agent_id as organizerAgentId, participant_agent_ids as participantAgentIds, scheduled_at as scheduledAt, duration_minutes as durationMinutes, agenda_json as agendaJson, notes_json as notesJson, decisions_json as decisionsJson, action_items_json as actionItemsJson, status, created_at as createdAt, updated_at as updatedAt from meetings"; +/** + * Sprint row shape returned by listSprints — used by + * GET /api/companies/:companyId/sprints. Mirrors the historical inline + * SQL projection. + */ +export type SprintListRow = { + id: string; + companyId: string; + name: string; + goal: string; + startDate: string; + endDate: string; + status: string; + retrospectiveNotes: string | null; + velocityPoints: number; + completedPoints: number; + createdAt: string; + updatedAt: string; +}; + /** * Knowledge entry row shape returned by listKnowledgeEntries — used by * GET /api/companies/:companyId/knowledge. Mirrors the historical inline @@ -151,5 +171,16 @@ export function buildKnowledgeMethods( deleteSprint(id: string, companyId: string) { getDb().prepare("delete from sprints where id = ? and company_id = ?").run(id, companyId); }, + + /** + * Returns all sprints for a company, ordered by start_date desc. + * Used by GET /api/companies/:companyId/sprints. + */ + listSprints(companyId: string): SprintListRow[] { + return readAll( + "select id, company_id as companyId, name, goal, start_date as startDate, end_date as endDate, status, retrospective_notes as retrospectiveNotes, velocity_points as velocityPoints, completed_points as completedPoints, created_at as createdAt, updated_at as updatedAt from sprints where company_id = ? order by start_date desc", + companyId, + ); + }, }; }