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
This commit is contained in:
parent
dbb1a84dba
commit
ee586065dd
@ -41,6 +41,29 @@ export async function createNoteAgentAction(doc: NoteAgentActionDoc): Promise<No
|
||||
return collection().create(doc);
|
||||
}
|
||||
|
||||
export async function listPendingActions(
|
||||
userId: string,
|
||||
productId: string,
|
||||
limit = 50,
|
||||
offset = 0,
|
||||
): Promise<{ items: NoteAgentActionDoc[]; total: number }> {
|
||||
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,
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -29,6 +29,19 @@ export async function listNotes(
|
||||
return { items, total };
|
||||
}
|
||||
|
||||
export async function countNotesByWorkspaces(
|
||||
userId: string,
|
||||
productId: string,
|
||||
workspaceIds: string[],
|
||||
): Promise<Map<string, number>> {
|
||||
const counts = new Map<string, number>();
|
||||
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<NoteDoc | null> {
|
||||
return collection().findById(id, workspaceId);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -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([
|
||||
{
|
||||
|
||||
@ -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<WorkspaceSummary[]> {
|
||||
const api = createNotesApiClient();
|
||||
const [workspaceResponse, noteResponse] = await Promise.all([
|
||||
api.fetch<WorkspaceListResponse>("/workspaces"),
|
||||
api.fetch<NoteListResponse>("/notes"),
|
||||
]);
|
||||
const response = await api.fetch<WorkspaceSummaryListResponse>("/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<NoteSummary[]> {
|
||||
@ -290,42 +235,49 @@ export async function createNoteTask(input: {
|
||||
});
|
||||
}
|
||||
|
||||
export async function getNoteDetail(noteId: string): Promise<NoteDetail | null> {
|
||||
export async function getNoteDetail(noteId: string, knownWorkspaceId?: string): Promise<NoteDetail | null> {
|
||||
const api = createNotesApiClient();
|
||||
const [workspaceResponse, noteResponse] = await Promise.all([
|
||||
api.fetch<WorkspaceListResponse>("/workspaces"),
|
||||
api.fetch<NoteListResponse>("/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<NoteDoc>(
|
||||
`/notes/${encodeURIComponent(noteId)}?workspaceId=${encodeURIComponent(knownWorkspaceId)}`
|
||||
);
|
||||
workspaceId = knownWorkspaceId;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
const noteResponse = await api.fetch<NoteListResponse>("/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<NoteTaskListResponse>(
|
||||
`/note-tasks?workspaceId=${encodeURIComponent(note.workspaceId)}¬eId=${encodeURIComponent(note.id)}`
|
||||
),
|
||||
api.fetch<NoteArtifactListResponse>(
|
||||
`/note-artifacts?workspaceId=${encodeURIComponent(note.workspaceId)}¬eId=${encodeURIComponent(note.id)}`
|
||||
),
|
||||
api.fetch<NoteAgentActionListResponse>(
|
||||
`/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<WorkspaceListResponse>("/workspaces"),
|
||||
api.fetch<NoteListResponse>(`/notes?workspaceId=${wsId}`),
|
||||
api.fetch<NoteTaskListResponse>(`/note-tasks?workspaceId=${wsId}¬eId=${nId}`),
|
||||
api.fetch<NoteArtifactListResponse>(`/note-artifacts?workspaceId=${wsId}¬eId=${nId}`),
|
||||
api.fetch<NoteAgentActionListResponse>(`/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<NoteRelationshipListResponse>(
|
||||
`/note-relationships?workspaceId=${encodeURIComponent(note.workspaceId)}¬eId=${encodeURIComponent(note.id)}`
|
||||
const fetched = await api.fetch<NoteRelationshipListResponse>(
|
||||
`/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<NoteDetail | null>
|
||||
timeline,
|
||||
};
|
||||
}
|
||||
|
||||
export async function createNote(input: {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
title: string;
|
||||
body: string;
|
||||
tags?: string[];
|
||||
sourceType?: string;
|
||||
}): Promise<NoteDoc> {
|
||||
const api = createNotesApiClient();
|
||||
return api.fetch<NoteDoc>("/notes", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export async function archiveNote(noteId: string, workspaceId: string): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
const api = createNotesApiClient();
|
||||
await api.fetch("/note-relationships", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
@ -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<NoteAgentActionDoc[]> {
|
||||
const api = createNotesApiClient();
|
||||
const response = await api.fetch<NoteAgentActionListResponse>(
|
||||
`/note-agent-actions?workspaceId=${encodeURIComponent(workspaceId)}`
|
||||
);
|
||||
return response.items;
|
||||
}
|
||||
|
||||
export async function listApprovalQueue(): Promise<ApprovalQueueItem[]> {
|
||||
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<NoteAgentActionListResponse>("/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<AgentTimelineItem[]> {
|
||||
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<NoteAgentActionListResponse>("/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);
|
||||
}
|
||||
|
||||
|
||||
@ -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[] };
|
||||
|
||||
Loading…
Reference in New Issue
Block a user