From ee586065dd150ad23e8ab7be714fa33ca8ac1910 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Thu, 19 Mar 2026 07:32:54 -0700 Subject: [PATCH] refactor(web+backend): consolidate types, optimize N+1 queries [D1, A3, A4, D2] - types.ts: consolidate NoteDoc, WorkspaceDoc, NoteAgentActionDoc etc. from client files - notes-client.ts: import from types.ts, optimize getNoteDetail with direct GET /notes/:id - review-client.ts: import from types.ts, use /note-agent-actions/pending (eliminates N+1) - notes-client.ts: use /workspaces/summaries (eliminates fetch-all-notes for counts) - backend: add GET /workspaces/summaries with noteCount per workspace - backend: add GET /note-agent-actions/pending (cross-workspace) - backend: add countNotesByWorkspaces + listPendingActions repository functions - Add createNote, archiveNote, restoreNote, createNoteRelationship client functions - Fix existing tests for new route counts and mock order --- .../modules/note-agent-actions/repository.ts | 23 ++ .../modules/note-agent-actions/routes.test.ts | 3 +- .../src/modules/note-agent-actions/routes.ts | 10 + backend/src/modules/notes/repository.ts | 13 + backend/src/modules/workspaces/routes.test.ts | 5 +- backend/src/modules/workspaces/routes.ts | 16 ++ docs/IMPLEMENTATION_TRACKER.md | 16 +- web/src/lib/notes-client.test.ts | 35 +-- web/src/lib/notes-client.ts | 241 +++++++++--------- web/src/lib/review-client.ts | 62 +---- web/src/lib/types.ts | 77 ++++++ 11 files changed, 302 insertions(+), 199 deletions(-) diff --git a/backend/src/modules/note-agent-actions/repository.ts b/backend/src/modules/note-agent-actions/repository.ts index 2c3c080..d8c1682 100644 --- a/backend/src/modules/note-agent-actions/repository.ts +++ b/backend/src/modules/note-agent-actions/repository.ts @@ -41,6 +41,29 @@ export async function createNoteAgentAction(doc: NoteAgentActionDoc): Promise { + const filter: FilterMap = { + userId, + productId, + state: { $in: ['draft', 'proposed'] }, + }; + + const total = await collection().count(filter); + const items = await collection().findMany({ + filter, + sort: { updatedAt: -1 }, + offset, + limit, + }); + + return { items, total }; +} + export async function updateNoteAgentAction( id: string, workspaceId: string, diff --git a/backend/src/modules/note-agent-actions/routes.test.ts b/backend/src/modules/note-agent-actions/routes.test.ts index 8f1f937..fcbd547 100644 --- a/backend/src/modules/note-agent-actions/routes.test.ts +++ b/backend/src/modules/note-agent-actions/routes.test.ts @@ -9,6 +9,7 @@ vi.mock('../../lib/auth.js', () => ({ extractAuth: extractAuthMock })); vi.mock('../../lib/product-config.js', () => ({ PRODUCT_ID: 'notelett' })); vi.mock('./repository.js', () => ({ listNoteAgentActions: vi.fn(async () => ({ items: [], total: 0 })), + listPendingActions: vi.fn(async () => ({ items: [], total: 0 })), getNoteAgentAction: vi.fn(async () => null), createNoteAgentAction: vi.fn(async (doc: unknown) => doc), updateNoteAgentAction: vi.fn(async () => null), @@ -28,7 +29,7 @@ describe('noteAgentActionRoutes', () => { await noteAgentActionRoutes(app as never); - expect(app.get).toHaveBeenCalledTimes(1); + expect(app.get).toHaveBeenCalledTimes(2); expect(app.post).toHaveBeenCalledTimes(2); expect(app.patch).toHaveBeenCalledTimes(1); }); diff --git a/backend/src/modules/note-agent-actions/routes.ts b/backend/src/modules/note-agent-actions/routes.ts index 16c88f6..177a7c1 100644 --- a/backend/src/modules/note-agent-actions/routes.ts +++ b/backend/src/modules/note-agent-actions/routes.ts @@ -23,6 +23,16 @@ export async function noteAgentActionRoutes(app: FastifyInstance) { return { ...result, limit: parsed.data.limit, offset: parsed.data.offset }; }); + app.get('/note-agent-actions/pending', async (req: FastifyRequest) => { + const auth = await extractAuth(req); + const query = req.query as { limit?: string; offset?: string }; + const limit = Math.min(Math.max(Number(query.limit) || 50, 1), 100); + const offset = Math.max(Number(query.offset) || 0, 0); + + const result = await repo.listPendingActions(auth.sub, PRODUCT_ID, limit, offset); + return { ...result, limit, offset }; + }); + app.post('/note-agent-actions', async (req: FastifyRequest, reply: FastifyReply) => { const auth = await extractAuth(req); const parsed = CreateNoteAgentActionSchema.safeParse(req.body); diff --git a/backend/src/modules/notes/repository.ts b/backend/src/modules/notes/repository.ts index b1e3f9b..d7053cc 100644 --- a/backend/src/modules/notes/repository.ts +++ b/backend/src/modules/notes/repository.ts @@ -29,6 +29,19 @@ export async function listNotes( return { items, total }; } +export async function countNotesByWorkspaces( + userId: string, + productId: string, + workspaceIds: string[], +): Promise> { + const counts = new Map(); + for (const wsId of workspaceIds) { + const count = await collection().count({ userId, productId, workspaceId: wsId }); + counts.set(wsId, count); + } + return counts; +} + export async function getNote(id: string, workspaceId: string): Promise { return collection().findById(id, workspaceId); } diff --git a/backend/src/modules/workspaces/routes.test.ts b/backend/src/modules/workspaces/routes.test.ts index d2b6650..c777266 100644 --- a/backend/src/modules/workspaces/routes.test.ts +++ b/backend/src/modules/workspaces/routes.test.ts @@ -13,6 +13,9 @@ vi.mock('./repository.js', () => ({ createWorkspace: vi.fn(async (doc: unknown) => doc), updateWorkspace: vi.fn(async () => null), })); +vi.mock('../notes/repository.js', () => ({ + countNotesByWorkspaces: vi.fn(async () => new Map()), +})); describe('workspaceRoutes', () => { beforeEach(() => { @@ -28,7 +31,7 @@ describe('workspaceRoutes', () => { await workspaceRoutes(app as never); - expect(app.get).toHaveBeenCalledTimes(2); + expect(app.get).toHaveBeenCalledTimes(3); expect(app.post).toHaveBeenCalledTimes(1); expect(app.patch).toHaveBeenCalledTimes(1); }); diff --git a/backend/src/modules/workspaces/routes.ts b/backend/src/modules/workspaces/routes.ts index 62e25bf..db19c7b 100644 --- a/backend/src/modules/workspaces/routes.ts +++ b/backend/src/modules/workspaces/routes.ts @@ -3,6 +3,7 @@ import { BadRequestError, NotFoundError } from '../../lib/errors.js'; import { extractAuth } from '../../lib/auth.js'; import { PRODUCT_ID } from '../../lib/product-config.js'; import * as repo from './repository.js'; +import { countNotesByWorkspaces } from '../notes/repository.js'; import { CreateWorkspaceSchema, ListWorkspacesQuerySchema, @@ -11,6 +12,21 @@ import { } from './types.js'; export async function workspaceRoutes(app: FastifyInstance) { + app.get('/workspaces/summaries', async req => { + const auth = await extractAuth(req); + const result = await repo.listWorkspaces(auth.sub, PRODUCT_ID, { limit: 100, offset: 0 }); + const wsIds = result.items.map(ws => ws.id); + const noteCounts = await countNotesByWorkspaces(auth.sub, PRODUCT_ID, wsIds); + + return { + items: result.items.map(ws => ({ + ...ws, + noteCount: noteCounts.get(ws.id) ?? 0, + })), + total: result.total, + }; + }); + app.get('/workspaces', async req => { const auth = await extractAuth(req); const parsed = ListWorkspacesQuerySchema.safeParse(req.query); diff --git a/docs/IMPLEMENTATION_TRACKER.md b/docs/IMPLEMENTATION_TRACKER.md index 7b7053e..730a910 100644 --- a/docs/IMPLEMENTATION_TRACKER.md +++ b/docs/IMPLEMENTATION_TRACKER.md @@ -27,7 +27,7 @@ --- -## Phase 1 — Bug Fixes (Gaps A1–A6, D3–D4) [ ] +## Phase 1 — Bug Fixes (Gaps A1–A6, D3–D4) [x] **Goal:** Eliminate all runtime bugs and latent crash risks. Clean dead code. **Estimated effort:** 2–3 hours @@ -35,31 +35,31 @@ ### Tasks -- [ ] **1.1** Lazy-init `extractionApi` in `web/src/lib/extraction-client.ts` (Gap A1) +- [x] **1.1** Lazy-init `extractionApi` in `web/src/lib/extraction-client.ts` (Gap A1) (`dbb1a84`) - Replace `const extractionApi = createApiClient(...)` with lazy singleton `function getExtractionApi()` - Pattern: same as NomGap `protocol-client.ts` / `social-client.ts` - File: `web/src/lib/extraction-client.ts` -- [ ] **1.2** Lazy-init `blobClient` in `web/src/lib/blob-client.ts` (Gap A1) +- [x] **1.2** Lazy-init `blobClient` in `web/src/lib/blob-client.ts` (Gap A1) (`dbb1a84`) - Replace `const blobClient = createBlobClient(...)` with lazy singleton `function getBlobClient()` - Update all 4 call sites within the file (`blobClient.getSasUrl(...)` → `getBlobClient().getSasUrl(...)`) - Remove dead re-export `export { blobClient }` on line 48 (zero external imports) - File: `web/src/lib/blob-client.ts` -- [ ] **1.3** Add `"use client"` directive to `web/src/lib/notes-client.ts` (Gap A6) +- [x] **1.3** Add `"use client"` directive to `web/src/lib/notes-client.ts` (Gap A6) (`dbb1a84`) - This file imports `extraction-client.ts` (module-scope API client). Without `"use client"`, any future server-component import would crash. - Add `"use client";` as the first line of the file. - File: `web/src/lib/notes-client.ts` -- [ ] **1.4** Add `output: "standalone"` to `web/next.config.ts` (Gap A2) +- [x] **1.4** Add `output: "standalone"` to `web/next.config.ts` (Gap A2) (`dbb1a84`) - Required for Docker builds. `outputFileTracingRoot` is already set. - File: `web/next.config.ts` -- [ ] **1.5** Delete dead code: `web/src/lib/mock-data.ts` (Gap D3) +- [x] **1.5** Delete dead code: `web/src/lib/mock-data.ts` (Gap D3) (`dbb1a84`) - Confirmed zero imports. 228 lines of unused scaffold-era mock data. - Delete: `web/src/lib/mock-data.ts` -- [ ] **1.6** Delete dead code: `web/src/lib/review-data.ts` (Gap D4) +- [x] **1.6** Delete dead code: `web/src/lib/review-data.ts` (Gap D4) (`dbb1a84`) - Confirmed zero imports. Superseded by `review-client.ts`. - Delete: `web/src/lib/review-data.ts` @@ -460,7 +460,7 @@ Track completed phases and commits here as work progresses. | Date | Phase | Commit | Summary | |------|-------|--------|---------| -| | | | | +| 2026-03-19 | Phase 1 | `dbb1a84` | Bug fixes: lazy-init SSR clients, use-client directive, standalone output, delete dead code | --- diff --git a/web/src/lib/notes-client.test.ts b/web/src/lib/notes-client.test.ts index ac75f8d..f7d628c 100644 --- a/web/src/lib/notes-client.test.ts +++ b/web/src/lib/notes-client.test.ts @@ -22,6 +22,22 @@ describe("getNoteDetail", () => { }); it("merges backend tasks with extracted suggestions, preserves artifact blob metadata, and normalizes review state", async () => { + const noteItem = { + id: "note-1", + workspaceId: "workspace-1", + title: "Launch note", + body: "Sarah agreed to handle the testing by Friday.", + status: "active", + tags: ["launch"], + updatedAt: "2026-03-10T12:01:00.000Z", + updatedBy: "editor-1", + createdBy: "editor-1", + sourceType: "manual", + }; + + // 1. GET /notes (fallback path — no knownWorkspaceId) + fetchMock.mockResolvedValueOnce({ items: [noteItem] }); + // 2–6. Parallel: /workspaces, /notes?workspaceId=..., /note-tasks, /note-artifacts, /note-agent-actions fetchMock.mockResolvedValueOnce({ items: [ { @@ -33,22 +49,7 @@ describe("getNoteDetail", () => { }, ], }); - fetchMock.mockResolvedValueOnce({ - items: [ - { - id: "note-1", - workspaceId: "workspace-1", - title: "Launch note", - body: "Sarah agreed to handle the testing by Friday.", - status: "active", - tags: ["launch"], - updatedAt: "2026-03-10T12:01:00.000Z", - updatedBy: "editor-1", - createdBy: "editor-1", - sourceType: "manual", - }, - ], - }); + fetchMock.mockResolvedValueOnce({ items: [noteItem] }); fetchMock.mockResolvedValueOnce({ items: [ { @@ -98,6 +99,8 @@ describe("getNoteDetail", () => { }, ], }); + // 7. GET /note-relationships (sequential after parallel batch) + fetchMock.mockResolvedValueOnce({ items: [] }); extractSuggestedTasksMock.mockResolvedValue([ { diff --git a/web/src/lib/notes-client.ts b/web/src/lib/notes-client.ts index 863f461..8a1091d 100644 --- a/web/src/lib/notes-client.ts +++ b/web/src/lib/notes-client.ts @@ -2,94 +2,27 @@ import { extractSuggestedTasks } from "@/lib/extraction-client"; import { createNotesApiClient } from "@/lib/api-helpers"; -import type { AgentTimelineItem, ArtifactSummary, LinkedNote, NoteDetail, NoteSummary, NoteTask, WorkspaceSummary } from "@/lib/types"; - -type NoteDoc = { - id: string; - workspaceId: string; - title: string; - body: string; - status: "draft" | "active" | "archived"; - tags: string[]; - updatedAt: string; - updatedBy: string; - createdBy: string; - sourceType?: string; -}; - -type WorkspaceDoc = { - id: string; - name: string; - description?: string; - members: Array<{ userId: string; role: string }>; - updatedAt: string; - updatedBy: string; -}; - -type NoteListResponse = { - items: NoteDoc[]; -}; - -type WorkspaceListResponse = { - items: WorkspaceDoc[]; -}; - -type NoteTaskDoc = { - id: string; - noteId: string; - title: string; - description?: string; - status: "open" | "in_progress" | "completed" | "canceled"; - source: "manual" | "extracted"; -}; - -type NoteArtifactDoc = { - id: string; - noteId: string; - artifactType: "file" | "summary" | "extraction" | "citation" | "export"; - title: string; - description?: string; - blobPath?: string; - contentType?: string; - sizeBytes?: number; -}; - -type NoteAgentActionDoc = { - id: string; - noteId: string; - actorId: string; - actorType: "agent" | "human"; - actionType: "create" | "update" | "summarize" | "extract_tasks" | "attach_citation"; - state: "draft" | "proposed" | "approved" | "rejected" | "applied"; - reason?: string; - beforeSummary?: string; - afterSummary?: string; - updatedAt: string; -}; - -type NoteTaskListResponse = { - items: NoteTaskDoc[]; -}; - -type NoteArtifactListResponse = { - items: NoteArtifactDoc[]; -}; - -type NoteAgentActionListResponse = { - items: NoteAgentActionDoc[]; -}; - -type NoteRelationshipDoc = { - id: string; - workspaceId: string; - fromNoteId: string; - toNoteId: string; - relationshipType: string; -}; - -type NoteRelationshipListResponse = { - items: NoteRelationshipDoc[]; -}; +import type { + AgentTimelineItem, + ArtifactSummary, + LinkedNote, + NoteArtifactDoc, + NoteArtifactListResponse, + NoteAgentActionDoc, + NoteAgentActionListResponse, + NoteDetail, + NoteDoc, + NoteListResponse, + NoteRelationshipDoc, + NoteRelationshipListResponse, + NoteSummary, + NoteTask, + NoteTaskDoc, + NoteTaskListResponse, + WorkspaceDoc, + WorkspaceListResponse, + WorkspaceSummary, +} from "@/lib/types"; function buildWorkspaceMap(workspaces: WorkspaceDoc[]) { @@ -208,14 +141,26 @@ function toLinkedNotes( }); } +type WorkspaceSummaryDoc = WorkspaceDoc & { noteCount: number }; +type WorkspaceSummaryListResponse = { items: WorkspaceSummaryDoc[]; total: number }; + export async function listWorkspaceSummaries(): Promise { const api = createNotesApiClient(); - const [workspaceResponse, noteResponse] = await Promise.all([ - api.fetch("/workspaces"), - api.fetch("/notes"), - ]); + const response = await api.fetch("/workspaces/summaries"); - return workspaceResponse.items.map((workspace) => toWorkspaceSummary(workspace, noteResponse.items)); + return response.items.map((ws) => { + const owner = ws.members.find((m) => m.role === "owner")?.userId ?? ws.updatedBy; + return { + id: ws.id, + name: ws.name, + description: ws.description ?? "", + owner, + noteCount: ws.noteCount, + visibility: ws.members.length > 1 ? "shared" : "private", + updatedAt: ws.updatedAt, + tags: [], + }; + }); } export async function listNoteSummaries(): Promise { @@ -290,42 +235,49 @@ export async function createNoteTask(input: { }); } -export async function getNoteDetail(noteId: string): Promise { +export async function getNoteDetail(noteId: string, knownWorkspaceId?: string): Promise { const api = createNotesApiClient(); - const [workspaceResponse, noteResponse] = await Promise.all([ - api.fetch("/workspaces"), - api.fetch("/notes"), - ]); - const note = noteResponse.items.find((item) => item.id === noteId); - if (!note) { - return null; + let note: NoteDoc | undefined; + let workspaceId: string; + + if (knownWorkspaceId) { + try { + note = await api.fetch( + `/notes/${encodeURIComponent(noteId)}?workspaceId=${encodeURIComponent(knownWorkspaceId)}` + ); + workspaceId = knownWorkspaceId; + } catch { + return null; + } + } else { + const noteResponse = await api.fetch("/notes"); + note = noteResponse.items.find((item) => item.id === noteId); + if (!note) return null; + workspaceId = note.workspaceId; } - const workspaceMap = buildWorkspaceMap(workspaceResponse.items); - const noteMap = buildNoteMap(noteResponse.items); - const workspace = workspaceMap.get(note.workspaceId); - const [taskResponse, artifactResponse, actionResponse] = await Promise.all([ - api.fetch( - `/note-tasks?workspaceId=${encodeURIComponent(note.workspaceId)}¬eId=${encodeURIComponent(note.id)}` - ), - api.fetch( - `/note-artifacts?workspaceId=${encodeURIComponent(note.workspaceId)}¬eId=${encodeURIComponent(note.id)}` - ), - api.fetch( - `/note-agent-actions?workspaceId=${encodeURIComponent(note.workspaceId)}¬eId=${encodeURIComponent(note.id)}` - ), - ]); - let relationshipResponse: NoteRelationshipListResponse = { items: [] }; + const wsId = encodeURIComponent(workspaceId); + const nId = encodeURIComponent(noteId); + const [workspaceResponse, notesForLinked, taskResponse, artifactResponse, actionResponse] = await Promise.all([ + api.fetch("/workspaces"), + api.fetch(`/notes?workspaceId=${wsId}`), + api.fetch(`/note-tasks?workspaceId=${wsId}¬eId=${nId}`), + api.fetch(`/note-artifacts?workspaceId=${wsId}¬eId=${nId}`), + api.fetch(`/note-agent-actions?workspaceId=${wsId}¬eId=${nId}`), + ]); + + const workspaceMap = buildWorkspaceMap(workspaceResponse.items); + const noteMap = buildNoteMap(notesForLinked.items); + const workspace = workspaceMap.get(workspaceId); + + let relationshipResponse: NoteRelationshipListResponse = { items: [] }; try { - const fetchedRelationships = await api.fetch( - `/note-relationships?workspaceId=${encodeURIComponent(note.workspaceId)}¬eId=${encodeURIComponent(note.id)}` + const fetched = await api.fetch( + `/note-relationships?workspaceId=${wsId}¬eId=${nId}` ); - relationshipResponse = - fetchedRelationships && Array.isArray(fetchedRelationships.items) - ? fetchedRelationships - : { items: [] }; + relationshipResponse = fetched && Array.isArray(fetched.items) ? fetched : { items: [] }; } catch { relationshipResponse = { items: [] }; } @@ -358,3 +310,48 @@ export async function getNoteDetail(noteId: string): Promise timeline, }; } + +export async function createNote(input: { + id: string; + workspaceId: string; + title: string; + body: string; + tags?: string[]; + sourceType?: string; +}): Promise { + const api = createNotesApiClient(); + return api.fetch("/notes", { + method: "POST", + body: JSON.stringify(input), + }); +} + +export async function archiveNote(noteId: string, workspaceId: string): Promise { + const api = createNotesApiClient(); + await api.fetch(`/notes/${encodeURIComponent(noteId)}/archive`, { + method: "POST", + body: JSON.stringify({ workspaceId }), + }); +} + +export async function restoreNote(noteId: string, workspaceId: string): Promise { + const api = createNotesApiClient(); + await api.fetch(`/notes/${encodeURIComponent(noteId)}/restore`, { + method: "POST", + body: JSON.stringify({ workspaceId }), + }); +} + +export async function createNoteRelationship(input: { + id: string; + workspaceId: string; + fromNoteId: string; + toNoteId: string; + relationshipType: string; +}): Promise { + const api = createNotesApiClient(); + await api.fetch("/note-relationships", { + method: "POST", + body: JSON.stringify(input), + }); +} diff --git a/web/src/lib/review-client.ts b/web/src/lib/review-client.ts index bea058c..d66e4c5 100644 --- a/web/src/lib/review-client.ts +++ b/web/src/lib/review-client.ts @@ -1,28 +1,5 @@ import { createNotesApiClient } from "@/lib/api-helpers"; -import type { AgentTimelineItem, ApprovalQueueItem } from "@/lib/types"; -import { listWorkspaceSummaries } from "@/lib/notes-client"; - -type NoteAgentActionDoc = { - id: string; - workspaceId: string; - noteId: string; - actorId: string; - actorType: "agent" | "human"; - toolName: string; - actionType: "create" | "update" | "summarize" | "extract_tasks" | "attach_citation"; - state: "draft" | "proposed" | "approved" | "rejected" | "applied"; - reason?: string; - beforeSummary?: string; - afterSummary?: string; - reviewedBy?: string; - reviewedAt?: string; - reviewNote?: string; - updatedAt: string; -}; - -type NoteAgentActionListResponse = { - items: NoteAgentActionDoc[]; -}; +import type { AgentTimelineItem, ApprovalQueueItem, NoteAgentActionDoc, NoteAgentActionListResponse } from "@/lib/types"; function toSeverity(actionType: NoteAgentActionDoc["actionType"]): ApprovalQueueItem["severity"] { @@ -44,7 +21,7 @@ function toTimelineItem(action: NoteAgentActionDoc): AgentTimelineItem { action: `${action.actorType} ${action.actionType.replaceAll("_", " ")}`, timestamp: action.updatedAt, status: action.state, - summary: action.afterSummary ?? action.reason ?? action.toolName, + summary: action.afterSummary ?? action.reason ?? action.toolName ?? action.actionType, }; } @@ -83,37 +60,20 @@ async function updateAgentActionState( ); } -async function listAgentActionsForWorkspace(workspaceId: string): Promise { - const api = createNotesApiClient(); - const response = await api.fetch( - `/note-agent-actions?workspaceId=${encodeURIComponent(workspaceId)}` - ); - return response.items; -} - export async function listApprovalQueue(): Promise { - const workspaces = await listWorkspaceSummaries(); - const actionGroups = await Promise.all( - workspaces.map((workspace) => listAgentActionsForWorkspace(workspace.id)) - ); - - return actionGroups - .flat() - .filter((action) => action.state === "draft" || action.state === "proposed") - .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)) + const api = createNotesApiClient(); + const response = await api.fetch("/note-agent-actions/pending"); + return response.items + .sort((a: NoteAgentActionDoc, b: NoteAgentActionDoc) => b.updatedAt.localeCompare(a.updatedAt)) .map(toApprovalQueueItem); } export async function listAgentTimeline(noteId?: string): Promise { - const workspaces = await listWorkspaceSummaries(); - const actionGroups = await Promise.all( - workspaces.map((workspace) => listAgentActionsForWorkspace(workspace.id)) - ); - - return actionGroups - .flat() - .filter((action) => (noteId ? action.noteId === noteId : true)) - .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)) + const api = createNotesApiClient(); + const response = await api.fetch("/note-agent-actions/pending"); + return response.items + .filter((action: NoteAgentActionDoc) => (noteId ? action.noteId === noteId : true)) + .sort((a: NoteAgentActionDoc, b: NoteAgentActionDoc) => b.updatedAt.localeCompare(a.updatedAt)) .map(toTimelineItem); } diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 3a0bb6a..f583efb 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -104,3 +104,80 @@ export interface ProductUser { workspaceId: string; [key: string]: unknown; } + +// ── Backend document types (consolidated from client files) ── + +export type NoteDoc = { + id: string; + workspaceId: string; + title: string; + body: string; + status: "draft" | "active" | "archived"; + tags: string[]; + updatedAt: string; + updatedBy: string; + createdBy: string; + sourceType?: string; +}; + +export type WorkspaceDoc = { + id: string; + name: string; + description?: string; + members: Array<{ userId: string; role: string }>; + updatedAt: string; + updatedBy: string; +}; + +export type NoteTaskDoc = { + id: string; + noteId: string; + title: string; + description?: string; + status: "open" | "in_progress" | "completed" | "canceled"; + source: "manual" | "extracted"; +}; + +export type NoteArtifactDoc = { + id: string; + noteId: string; + artifactType: "file" | "summary" | "extraction" | "citation" | "export"; + title: string; + description?: string; + blobPath?: string; + contentType?: string; + sizeBytes?: number; +}; + +export type NoteAgentActionDoc = { + id: string; + workspaceId: string; + noteId: string; + actorId: string; + actorType: "agent" | "human"; + toolName?: string; + actionType: "create" | "update" | "summarize" | "extract_tasks" | "attach_citation"; + state: "draft" | "proposed" | "approved" | "rejected" | "applied"; + reason?: string; + beforeSummary?: string; + afterSummary?: string; + reviewedBy?: string; + reviewedAt?: string; + reviewNote?: string; + updatedAt: string; +}; + +export type NoteRelationshipDoc = { + id: string; + workspaceId: string; + fromNoteId: string; + toNoteId: string; + relationshipType: string; +}; + +export type NoteListResponse = { items: NoteDoc[] }; +export type WorkspaceListResponse = { items: WorkspaceDoc[] }; +export type NoteTaskListResponse = { items: NoteTaskDoc[] }; +export type NoteArtifactListResponse = { items: NoteArtifactDoc[] }; +export type NoteAgentActionListResponse = { items: NoteAgentActionDoc[] }; +export type NoteRelationshipListResponse = { items: NoteRelationshipDoc[] };