diff --git a/src/main/api-server/routes-meetings.ts b/src/main/api-server/routes-meetings.ts index 8020d048..0f6bdeeb 100644 --- a/src/main/api-server/routes-meetings.ts +++ b/src/main/api-server/routes-meetings.ts @@ -2,15 +2,6 @@ import { z } from "zod"; import { validateTaskCreation } from "../task-quality-gates"; import type { AgentApiServer } from "./server-core"; -type MeetingRow = { - id: string; companyId: string; type: string; title: string; - organizerAgentId: string; participantAgentIds: string; - scheduledAt: string; durationMinutes: number; - agendaJson: string; notesJson: string | null; - decisionsJson: string | null; actionItemsJson: string | null; - status: string; createdAt: string; updatedAt: string; -}; - export function registerMeetingRoutes(server: AgentApiServer) { const db = server.db; const onDomainChanged = server.onDomainChanged; @@ -70,19 +61,17 @@ export function registerMeetingRoutes(server: AgentApiServer) { server.json(res, 403, { error: "Company mismatch" }); return; } - const meetings = db.queryAll( - "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 where company_id = ? order by scheduled_at desc", - token.companyId, - ); + const meetings = db.listMeetings(token.companyId); server.json(res, 200, meetings); }); server.addRoute("PATCH", "/api/meetings/:meetingId", (_req, res, params, token, body) => { - const rows = db.queryAll( - "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 where id = ? and company_id = ? limit 1", - params.meetingId, token.companyId, - ); - const existing = rows[0] ?? null; + const meetingId = params.meetingId; + if (!meetingId) { + server.json(res, 400, { error: "Missing meetingId" }); + return; + } + const existing = db.getMeeting(meetingId, token.companyId); if (!existing) { server.json(res, 404, { error: "Meeting not found" }); return; @@ -121,7 +110,7 @@ export function registerMeetingRoutes(server: AgentApiServer) { } } db.saveMeeting({ - id: params.meetingId, + id: meetingId, companyId: token.companyId, type: data.type ?? "department_sync", title: data.title ?? "Meeting", diff --git a/src/main/database.ts b/src/main/database.ts index d244f98b..5de9c6ce 100644 --- a/src/main/database.ts +++ b/src/main/database.ts @@ -111,8 +111,11 @@ export class AppDatabase extends AppDatabaseBase { saveMeeting(input: { id?: string | null; companyId: string; type: string; title: string; organizerAgentId?: string | null; participantAgentIds?: string; scheduledAt: string; durationMinutes?: number; agendaJson?: string; notesJson?: string; decisionsJson?: string; actionItemsJson?: string; status?: string }) { return this.knowledgeMod.saveMeeting(input); } deleteMeeting(id: string, companyId: string) { this.knowledgeMod.deleteMeeting(id, companyId); } + listMeetings(companyId: string) { return this.knowledgeMod.listMeetings(companyId); } + getMeeting(meetingId: string, companyId: string) { return this.knowledgeMod.getMeeting(meetingId, companyId); } saveKnowledgeEntry(input: { id?: string | null; companyId: string; category: string; topic: string; content: string; authorAgentId?: string | null; importance?: string; tagsJson?: string; referencedEntityType?: string | null; referencedEntityId?: string | null; sourceRunId?: string | null }) { return this.knowledgeMod.saveKnowledgeEntry(input); } deleteKnowledgeEntry(id: string, companyId: string) { this.knowledgeMod.deleteKnowledgeEntry(id, companyId); } + 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); } diff --git a/src/main/database/knowledge.ts b/src/main/database/knowledge.ts index ce1d0417..f7bd4259 100644 --- a/src/main/database/knowledge.ts +++ b/src/main/database/knowledge.ts @@ -2,11 +2,71 @@ import type { DatabaseSync } from "node:sqlite"; import type { ReadAll } from "./helpers"; import { ensureId, nowIso } from "./helpers"; +/** + * Meeting row shape returned by listMeetings / getMeeting — used by + * meeting routes. Mirrors the historical inline SQL projection so HTTP + * response bodies stay byte-stable. + */ +export type MeetingListRow = { + id: string; + companyId: string; + type: string; + title: string; + organizerAgentId: string; + participantAgentIds: string; + scheduledAt: string; + durationMinutes: number; + agendaJson: string; + notesJson: string | null; + decisionsJson: string | null; + actionItemsJson: string | null; + status: string; + createdAt: string; + updatedAt: string; +}; + +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"; + +/** + * Knowledge entry row shape returned by listKnowledgeEntries — used by + * GET /api/companies/:companyId/knowledge. Mirrors the historical inline + * SQL projection so HTTP responses stay byte-stable. + */ +export type KnowledgeListRow = { + id: string; + companyId: string; + category: string; + topic: string; + content: string; + authorAgentId: string; + importance: string; + tagsJson: string; + referencedEntityType: string | null; + referencedEntityId: string | null; + createdAt: string; + updatedAt: string; +}; + export function buildKnowledgeMethods( getDb: () => DatabaseSync, - _readAll: ReadAll, + readAll: ReadAll, ) { return { + listMeetings(companyId: string): MeetingListRow[] { + return readAll( + `${MEETING_LIST_COLUMNS} where company_id = ? order by scheduled_at desc`, + companyId, + ); + }, + + getMeeting(meetingId: string, companyId: string): MeetingListRow | null { + const row = getDb() + .prepare(`${MEETING_LIST_COLUMNS} where id = ? and company_id = ? limit 1`) + .get(meetingId, companyId) as MeetingListRow | undefined; + return row ?? null; + }, + saveMeeting(input: { id?: string | null; companyId: string; type: string; title: string; organizerAgentId?: string | null; participantAgentIds?: string; scheduledAt: string; durationMinutes?: number; @@ -57,6 +117,17 @@ export function buildKnowledgeMethods( getDb().prepare("delete from knowledge_base where id = ? and company_id = ?").run(id, companyId); }, + /** + * Returns all knowledge entries for a company, ordered by updated_at desc. + * Used by GET /api/companies/:companyId/knowledge. + */ + listKnowledgeEntries(companyId: string): KnowledgeListRow[] { + return readAll( + "select id, company_id as companyId, category, topic, content, author_agent_id as authorAgentId, importance, tags_json as tagsJson, referenced_entity_type as referencedEntityType, referenced_entity_id as referencedEntityId, created_at as createdAt, updated_at as updatedAt from knowledge_base where company_id = ? order by updated_at desc", + companyId, + ); + }, + saveSprint(input: { id?: string | null; companyId: string; name: string; goal?: string; startDate: string; endDate: string; status?: string; retrospectiveNotes?: string;