From a5b0a89527d8ce792d326b8fd0faca0bc0cd4e6c Mon Sep 17 00:00:00 2001 From: Saravana Achu Mac Date: Sun, 29 Mar 2026 20:49:13 -0700 Subject: [PATCH] feat: add Pagination component, file upload, fix N+1, remove unused deps Phase 3 (Web UX Polish) of the execution roadmap: - Add reusable Pagination component for list views - Fix getNoteDetail to avoid fetching all notes when workspaceId is unknown - Add file upload button to ArtifactPanel using uploadArtifact() from blob-client - Remove unused zustand and zod from web dependencies - SSR safety already addressed via existing lazy-init patterns Made-with: Cursor --- docs/AGENT_TASK_ROADMAP.md | 12 ++++---- web/package.json | 4 +-- web/src/components/ArtifactPanel.tsx | 42 ++++++++++++++++++++++++++-- web/src/components/Pagination.tsx | 39 ++++++++++++++++++++++++++ web/src/lib/notes-client.ts | 12 +++++--- 5 files changed, 93 insertions(+), 16 deletions(-) create mode 100644 web/src/components/Pagination.tsx diff --git a/docs/AGENT_TASK_ROADMAP.md b/docs/AGENT_TASK_ROADMAP.md index 088cb63..94dd1ef 100644 --- a/docs/AGENT_TASK_ROADMAP.md +++ b/docs/AGENT_TASK_ROADMAP.md @@ -105,33 +105,33 @@ cd web && pnpm run typecheck && pnpm test && pnpm run build ## Phase 2 — Backend Hardening -- [ ] **2.1** Add integration tests for all route modules using `buildTestApp()` + Fastify `.inject()` +- [x] **2.1** Add integration tests for all route modules (already complete) — [`8d84bcb`](https://github.com/saravanakumardb1/learning_ai_notes/commit/8d84bcb) using `buildTestApp()` + Fastify `.inject()` - Current route tests only check handler registration counts - Need real request/response tests: happy path, validation errors, auth enforcement, 404s - Modules needing inject tests: notes, workspaces, note-relationships, note-tasks, note-artifacts, note-agent-actions, saved-views - Files: `backend/src/modules/*/routes.integration.test.ts` (7 new files) -- [ ] **2.2** Add DELETE endpoints for notes, workspaces, tasks, artifacts, relationships +- [x] **2.2** Add DELETE endpoints for notes, workspaces, tasks, artifacts, relationships — [`8d84bcb`](https://github.com/saravanakumardb1/learning_ai_notes/commit/8d84bcb) - Currently only saved-views has DELETE - Notes support archive but not hard delete — add soft-delete with a `deletedAt` timestamp - Files: `backend/src/modules/*/routes.ts` + `repository.ts` -- [ ] **2.3** Enforce role-based access on REST routes +- [x] **2.3** Enforce role-based access on REST routes — [`8d84bcb`](https://github.com/saravanakumardb1/learning_ai_notes/commit/8d84bcb) - `requireRole` is exported and tested but not used on any REST route - At minimum: write routes (POST/PATCH/DELETE) should require `editor` or `admin` role - File: every `routes.ts` file -- [ ] **2.4** Enforce workspace member authorization +- [x] **2.4** Enforce workspace member authorization — [`8d84bcb`](https://github.com/saravanakumardb1/learning_ai_notes/commit/8d84bcb) - `workspace.members` array is stored but never checked - Note CRUD should verify the requesting user is a member of the target workspace - Files: `backend/src/modules/notes/routes.ts`, `backend/src/modules/workspaces/repository.ts` -- [ ] **2.5** Activate telemetry — call `trackEvent()` on key business actions +- [x] **2.5** Activate telemetry — call `trackEvent()` on key business actions — [`8d84bcb`](https://github.com/saravanakumardb1/learning_ai_notes/commit/8d84bcb) - Buffer and flush infrastructure exist but no route handler actually calls `trackEvent()` - Track: note.created, note.updated, note.archived, note.searched, workspace.created, agentAction.approved, agentAction.rejected - Files: all `routes.ts` files + `backend/src/lib/telemetry.ts` -- [ ] **2.6** Activate feature flags in route logic +- [x] **2.6** Activate feature flags in route logic — [`8d84bcb`](https://github.com/saravanakumardb1/learning_ai_notes/commit/8d84bcb) - 6 flags registered with defaults but no route checks `isFeatureEnabled()` - At minimum: gate MCP write tools behind `mcp.enabled`, gate export behind a flag - Files: `backend/src/lib/feature-flags.ts`, relevant route files diff --git a/web/package.json b/web/package.json index 318e4de..2212e91 100644 --- a/web/package.json +++ b/web/package.json @@ -33,9 +33,7 @@ "sonner": "^2.0.0", "next": "16.1.6", "react": "19.2.0", - "react-dom": "19.2.0", - "zod": "^4.3.6", - "zustand": "^5.0.11" + "react-dom": "19.2.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/web/src/components/ArtifactPanel.tsx b/web/src/components/ArtifactPanel.tsx index 010edda..8c41e91 100644 --- a/web/src/components/ArtifactPanel.tsx +++ b/web/src/components/ArtifactPanel.tsx @@ -1,7 +1,8 @@ "use client"; -import { useState } from "react"; -import { getArtifactReadUrl } from "@/lib/blob-client"; +import { useRef, useState } from "react"; +import { getArtifactReadUrl, uploadArtifact } from "@/lib/blob-client"; +import { toast } from "@/lib/toast"; import type { ArtifactSummary } from "@/lib/types"; export function ArtifactPanel({ @@ -19,6 +20,8 @@ export function ArtifactPanel({ isCreating?: boolean; }) { const [openingId, setOpeningId] = useState(null); + const [uploading, setUploading] = useState(false); + const fileInputRef = useRef(null); const [title, setTitle] = useState(""); const [artifactType, setArtifactType] = useState<"file" | "summary" | "extraction" | "citation" | "export">("file"); const [description, setDescription] = useState(""); @@ -38,6 +41,29 @@ export function ArtifactPanel({ } } + async function handleFileUpload(e: React.ChangeEvent) { + const file = e.target.files?.[0]; + if (!file) return; + + setUploading(true); + try { + const path = `artifacts/${crypto.randomUUID()}/${file.name}`; + const result = await uploadArtifact(file, path); + await onCreate({ + title: file.name, + artifactType: "file", + description: `Uploaded file (${(result.sizeBytes / 1024).toFixed(1)} KB)`, + blobPath: result.blobPath, + }); + toast.success("File uploaded"); + } catch (err) { + toast.error(err instanceof Error ? err.message : "Upload failed"); + } finally { + setUploading(false); + if (fileInputRef.current) fileInputRef.current.value = ""; + } + } + async function handleCreateArtifact() { if (!title.trim()) { return; @@ -101,7 +127,17 @@ export function ArtifactPanel({ placeholder="Description" style={{ minHeight: 96, resize: "vertical" }} /> -
+
+ + + + Page {currentPage} of {totalPages} + + +
+ ); +} diff --git a/web/src/lib/notes-client.ts b/web/src/lib/notes-client.ts index 956248e..c77e2b3 100644 --- a/web/src/lib/notes-client.ts +++ b/web/src/lib/notes-client.ts @@ -251,10 +251,14 @@ export async function getNoteDetail(noteId: string, knownWorkspaceId?: string): return null; } } else { - const noteResponse = await api.fetch("/notes"); - note = noteResponse.items.find((item) => item.id === noteId); - if (!note) return null; - workspaceId = note.workspaceId; + try { + const noteResponse = await api.fetch(`/notes?search=${encodeURIComponent(noteId)}&limit=1`); + note = noteResponse.items.find((item) => item.id === noteId); + if (!note) return null; + workspaceId = note.workspaceId; + } catch { + return null; + } } const wsId = encodeURIComponent(workspaceId);