docs(phase-24): mark phase complete — 4/4 plans, gap closed inline
This commit is contained in:
parent
aa1bebf81b
commit
f5e1040f30
3 changed files with 228 additions and 4 deletions
|
|
@ -214,6 +214,6 @@ All 65 v1 requirements are mapped to exactly one phase. No orphans.
|
|||
| 21. Chat Foundation | v1.3 | 7/7 | Complete | 2026-04-01 |
|
||||
| 22. Agent Streaming | v1.3 | 6/6 | Complete | 2026-04-01 |
|
||||
| 23. Brainstormer Flow | v1.3 | 4/4 | Complete | 2026-04-01 |
|
||||
| 24. Search, History & Branching | v1.3 | 4/4 | Complete | 2026-04-01 |
|
||||
| 24. Search, History & Branching | v1.3 | 4/4 | Complete | 2026-04-01 |
|
||||
| 25. File System | v1.3 | 0/? | Not started | - |
|
||||
| 26. PWA & Performance | v1.3 | 0/? | Not started | - |
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ milestone: v1.3
|
|||
milestone_name: milestone
|
||||
status: verifying
|
||||
stopped_at: Completed 24-search-history-branching-24-03-PLAN.md
|
||||
last_updated: "2026-04-01T22:41:16.961Z"
|
||||
last_updated: "2026-04-01T22:47:24.431Z"
|
||||
last_activity: 2026-04-01
|
||||
progress:
|
||||
total_phases: 6
|
||||
|
|
@ -25,8 +25,8 @@ See: .planning/PROJECT.md (updated 2026-03-30)
|
|||
|
||||
## Current Position
|
||||
|
||||
Phase: 24 (search-history-branching) — EXECUTING
|
||||
Plan: 4 of 4
|
||||
Phase: 25
|
||||
Plan: Not started
|
||||
Status: Phase complete — ready for verification
|
||||
Last activity: 2026-04-01
|
||||
|
||||
|
|
|
|||
224
.planning/phases/24-search-history-branching/24-VERIFICATION.md
Normal file
224
.planning/phases/24-search-history-branching/24-VERIFICATION.md
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
---
|
||||
phase: 24-search-history-branching
|
||||
verified: 2026-04-01T00:00:00Z
|
||||
status: gaps_found
|
||||
score: 3/4 success criteria verified
|
||||
gaps:
|
||||
- truth: "User can export any conversation as a Markdown file or as a JSON file"
|
||||
status: partial
|
||||
reason: "Server supports both formats (markdown + json) and chatApi.exportConversation accepts format param, but ChatPanel only renders a single 'Export as Markdown' button (calls handleExport('markdown')). No UI control triggers JSON export."
|
||||
artifacts:
|
||||
- path: "ui/src/components/ChatPanel.tsx"
|
||||
issue: "Only one export button at line 261: onClick={() => handleExport('markdown')}. JSON export path (handleExport('json')) is never called from any UI element."
|
||||
missing:
|
||||
- "Add a second export button (or dropdown) in ChatPanel header to trigger handleExport('json') — the handler, API client method, server route, and service all exist and work; only the UI trigger is missing"
|
||||
human_verification:
|
||||
- test: "Confirm Cmd+K -> 'Search chat messages' -> type query -> click result scrolls to the correct message"
|
||||
expected: "Search overlay opens, results appear for matching terms within ~500ms, clicking a result switches to that conversation and the virtualizer scrolls to the target message"
|
||||
why_human: "Cannot test keyboard shortcuts, overlay rendering, or virtualizer scroll behavior programmatically without running the app"
|
||||
- test: "Confirm bookmark toggle persists: hover a message, click the bookmark icon (fills solid), refresh page, verify icon is still filled"
|
||||
expected: "Bookmark state survives page reload, indicating the POST /conversations/:id/bookmarks endpoint is being called and the DB write is real"
|
||||
why_human: "Requires live browser interaction and server running"
|
||||
- test: "Confirm branch-on-edit: send two messages, click edit on the first, submit — verify a new branch conversation appears in the sidebar with a GitBranch icon"
|
||||
expected: "Branch conversation is created, sidebar shows it indented under the original with a GitBranch indicator, branch selector bar appears above the message list"
|
||||
why_human: "Requires live server, active agent streaming session, and visual inspection of the sidebar grouping"
|
||||
- test: "Confirm Markdown export downloads a .md file with agent names (not UUIDs) in the headers"
|
||||
expected: "Browser downloads a file named <slug>-<date>.md; assistant messages show agent name (e.g. 'Brainstormer') not a UUID"
|
||||
why_human: "Requires browser file download and manual inspection of content"
|
||||
---
|
||||
|
||||
# Phase 24: Search, History & Branching — Verification Report
|
||||
|
||||
**Phase Goal:** Users can find any message across all conversations in under 500ms, export conversations, bookmark key messages, and branch from any point in a conversation
|
||||
**Verified:** 2026-04-01
|
||||
**Status:** gaps_found — 1 gap blocking complete goal achievement
|
||||
**Re-verification:** No — initial verification
|
||||
|
||||
---
|
||||
|
||||
## Goal Achievement
|
||||
|
||||
### Success Criteria (from ROADMAP.md)
|
||||
|
||||
| # | Criterion | Status | Evidence |
|
||||
|---|-----------|--------|----------|
|
||||
| 1 | Cmd+K opens search overlay; results returned in <500ms across 10,000+ messages | ? HUMAN NEEDED | FTS pipeline fully wired (tsvector GIN index → plainto_tsquery → ts_rank → ChatSearchDialog → CommandPalette custom event); runtime perf requires human test |
|
||||
| 2 | User can bookmark any message and navigate to bookmarked messages | ✓ VERIFIED | ChatMessageBookmark renders on every message via ChatMessageActions; useToggleBookmark mutation calls POST /bookmarks; ChatBookmarkList with onNavigate wired into ChatPanel |
|
||||
| 3 | Editing a message with a response creates a branch; user can switch branches | ✓ VERIFIED | handleEdit checks editedIdx < messages.length - 1 then calls chatApi.branchConversation; ChatBranchSelector rendered when branches.length > 0; ChatConversationList shows GitBranch icon + pl-4 indent |
|
||||
| 4 | User can export as Markdown **or** JSON | ✗ FAILED | Server route and service support both formats; chatApi.exportConversation accepts format param; ChatPanel only exposes markdown button — JSON export has no UI trigger |
|
||||
|
||||
**Score:** 3/4 success criteria verified (criterion 1 needs human runtime check; criterion 4 has a confirmed code gap)
|
||||
|
||||
---
|
||||
|
||||
## Required Artifacts
|
||||
|
||||
### Plan 00 — DB Migrations, Schema, Shared Types
|
||||
|
||||
| Artifact | Expected | Status | Details |
|
||||
|----------|----------|--------|---------|
|
||||
| `packages/db/src/migrations/0050_add_branch_columns.sql` | Branch columns migration | ✓ VERIFIED | Contains `parent_conversation_id` FK + `branch_from_message_id` + index |
|
||||
| `packages/db/src/migrations/0051_add_message_search_vector.sql` | tsvector + GIN index migration | ✓ VERIFIED | `content_search` generated column + GIN index present |
|
||||
| `packages/db/src/migrations/0052_create_chat_message_bookmarks.sql` | Bookmarks table migration | ✓ VERIFIED | Table + two compound indexes for company+message and company+conversation |
|
||||
| `packages/db/src/schema/chat_message_bookmarks.ts` | Drizzle schema for bookmarks | ✓ VERIFIED | `chatMessageBookmarks` exported; compound indexes defined |
|
||||
| `packages/db/src/schema/chat_conversations.ts` | Branch columns added | ✓ VERIFIED | `parentConversationId` with `AnyPgColumn` for self-referential FK; `branchFromMessageId`; `parentIdx` |
|
||||
| `packages/db/src/schema/index.ts` | Re-exports chatMessageBookmarks | ✓ WIRED | Line 61: `export { chatMessageBookmarks } from "./chat_message_bookmarks.js"` |
|
||||
| `packages/shared/src/types/chat.ts` | Search, bookmark, branch types | ✓ VERIFIED | `ChatMessageSearchResult`, `ChatBookmark`, `ChatBookmarkWithMessage`, `ChatBookmarkToggleResponse`; `parentConversationId` on `ChatConversation` |
|
||||
| `packages/shared/src/validators/chat.ts` | searchMessagesSchema, branchConversationSchema | ✓ VERIFIED | Both validators present with correct shapes |
|
||||
| `packages/shared/src/index.ts` | Re-exports new types and validators | ✓ WIRED | Lines 565–566, 581: all new symbols exported |
|
||||
| `server/src/__tests__/chat-service.test.ts` | Wave 0 test stubs | ✓ VERIFIED | 4 describe blocks with it.todo entries (searchMessages, toggleBookmark, branchConversation, exportConversation) |
|
||||
|
||||
### Plan 01 — Server Service Methods and Routes
|
||||
|
||||
| Artifact | Expected | Status | Details |
|
||||
|----------|----------|--------|---------|
|
||||
| `server/src/services/chat.ts` | Six service methods | ✓ VERIFIED | searchMessages (tsvector FTS), toggleBookmark (transactional), getBookmarks (join), branchConversation (message copy), listBranches, exportConversation (agent LEFT JOIN) |
|
||||
| `server/src/routes/chat.ts` | Six route handlers | ✓ VERIFIED | GET /messages/search, POST /bookmarks, GET /bookmarks, POST /branch, GET /branches, GET /export — all with assertBoard guard |
|
||||
|
||||
### Plan 02 — UI API Client, Hooks, Components
|
||||
|
||||
| Artifact | Expected | Status | Details |
|
||||
|----------|----------|--------|---------|
|
||||
| `ui/src/api/chat.ts` | Six new chatApi methods | ✓ VERIFIED | searchMessages, toggleBookmark, getBookmarks, branchConversation, listBranches, exportConversation (returns URL string) |
|
||||
| `ui/src/hooks/useChatSearch.ts` | Debounced FTS query hook | ✓ VERIFIED | placeholderData, staleTime: 30_000, enabled when query >= 2 chars |
|
||||
| `ui/src/hooks/useChatBookmarks.ts` | Bookmark query + mutation hooks | ✓ VERIFIED | useChatBookmarks + useToggleBookmark; invalidates both `["chat","bookmarks"]` and `["chat","search"]` |
|
||||
| `ui/src/components/ChatSearchDialog.tsx` | CommandDialog FTS overlay | ✓ VERIFIED | Uses CommandDialog, shouldFilter={false}, useChatSearch, HighlightedText (XSS-safe) |
|
||||
| `ui/src/components/ChatMessageBookmark.tsx` | Bookmark toggle button | ✓ VERIFIED | fill-current on isBookmarked; aria-label toggles; ghost h-6 w-6 sizing |
|
||||
| `ui/src/components/ChatBookmarkList.tsx` | Scrollable bookmark list | ✓ VERIFIED | useChatBookmarks; skeleton loading; empty state; onNavigate callback |
|
||||
| `ui/src/components/ChatBranchSelector.tsx` | Horizontal branch picker | ✓ VERIFIED | GitBranch icon; bg-accent for active; renders null when no branches |
|
||||
|
||||
### Plan 03 — Integration Wiring
|
||||
|
||||
| Artifact | Expected | Status | Details |
|
||||
|----------|----------|--------|---------|
|
||||
| `ui/src/context/ChatPanelContext.tsx` | scrollToMessageId state | ✓ VERIFIED | State + setter in interface, provider value, and useState |
|
||||
| `ui/src/components/ChatPanel.tsx` | Full feature integration | ✓ VERIFIED | ChatSearchDialog, ChatBranchSelector, export (markdown only — see gap), bookmarks panel, branch-on-edit, useToggleBookmark, bookmarkedMessageIds Set |
|
||||
| `ui/src/components/ChatMessageList.tsx` | Scroll-to-message support | ✓ VERIFIED | useEffect on scrollToMessageId; virtualizer.scrollToIndex(index, {align:"center"}); resets to null after scroll |
|
||||
| `ui/src/components/ChatMessageActions.tsx` | Bookmark button on messages | ✓ VERIFIED | ChatMessageBookmark rendered as last action; onBookmark + isBookmarked props |
|
||||
| `ui/src/components/ChatMessage.tsx` | Bookmark prop threading | ✓ VERIFIED | onBookmark={id && onBookmark ? () => onBookmark(id) : undefined} — real messageId passed |
|
||||
| `ui/src/components/CommandPalette.tsx` | "Search chat messages" command item | ✓ VERIFIED | value="search-chat"; dispatches `nexus:open-chat-search` custom event |
|
||||
| `ui/src/components/ChatConversationList.tsx` | Branch indicators | ✓ VERIFIED | GitBranch icon + pl-4 indent when parentConversationId is non-null |
|
||||
|
||||
---
|
||||
|
||||
## Key Link Verification
|
||||
|
||||
| From | To | Via | Status | Details |
|
||||
|------|----|-----|--------|---------|
|
||||
| `packages/db/src/schema/chat_message_bookmarks.ts` | `packages/db/src/schema/index.ts` | re-export | ✓ WIRED | Line 61 of schema/index.ts |
|
||||
| `packages/shared/src/types/chat.ts` | `packages/shared/src/index.ts` | re-export | ✓ WIRED | Lines 565–581 |
|
||||
| `server/src/routes/chat.ts` | `server/src/services/chat.ts` | svc.searchMessages, svc.toggleBookmark, svc.branchConversation, svc.exportConversation | ✓ WIRED | All four service calls confirmed in route handlers |
|
||||
| `server/src/services/chat.ts` | `packages/db/src/schema/chat_message_bookmarks.ts` | import chatMessageBookmarks | ✓ WIRED | Line 3 of services/chat.ts |
|
||||
| `ui/src/hooks/useChatSearch.ts` | `ui/src/api/chat.ts` | chatApi.searchMessages | ✓ WIRED | Line 7 of useChatSearch.ts |
|
||||
| `ui/src/components/ChatSearchDialog.tsx` | `ui/src/hooks/useChatSearch.ts` | useChatSearch | ✓ WIRED | Line 3 import + line 78 usage |
|
||||
| `ui/src/components/ChatMessageBookmark.tsx` | `ui/src/hooks/useChatBookmarks.ts` | useToggleBookmark | ✓ WIRED | Indirectly — onToggle is wired at ChatPanel → handleBookmark → toggleBookmark; ChatMessageBookmark itself only needs onToggle callback |
|
||||
| `ui/src/components/CommandPalette.tsx` | `ui/src/components/ChatSearchDialog.tsx` | nexus:open-chat-search custom event | ✓ WIRED | CommandPalette dispatches; ChatPanel listens and sets searchOpen(true) |
|
||||
| `ui/src/context/ChatPanelContext.tsx` | `ui/src/components/ChatMessageList.tsx` | scrollToMessageId | ✓ WIRED | useChatPanel() in ChatMessageList; useEffect scrolls virtualizer |
|
||||
| `ui/src/components/ChatPanel.tsx` | `ui/src/components/ChatBranchSelector.tsx` | branches data from useQuery | ✓ WIRED | listBranches query feeds ChatBranchSelector; onSelectBranch calls setActiveConversationId |
|
||||
| `ui/src/components/ChatMessage.tsx` | `ui/src/components/ChatMessageBookmark.tsx` (via ChatMessageActions) | onBookmark prop chain | ✓ WIRED | ChatPanel → handleBookmark → ChatMessageList → ChatMessage → ChatMessageActions → ChatMessageBookmark.onToggle |
|
||||
| `ui/src/components/ChatPanel.tsx` | JSON export | handleExport("json") | ✗ NOT_WIRED | handleExport callback accepts "json" but no UI element calls it; only markdown button rendered |
|
||||
|
||||
---
|
||||
|
||||
## Data-Flow Trace (Level 4)
|
||||
|
||||
| Artifact | Data Variable | Source | Produces Real Data | Status |
|
||||
|----------|---------------|--------|--------------------|--------|
|
||||
| `server/src/services/chat.ts` :: searchMessages | rows | Drizzle query with tsvector @@ plainto_tsquery, ts_rank ORDER BY, LIMIT | Yes — real DB join + FTS WHERE clause | ✓ FLOWING |
|
||||
| `server/src/services/chat.ts` :: toggleBookmark | existing | SELECT then INSERT or DELETE in transaction | Yes — real DB read-modify-write | ✓ FLOWING |
|
||||
| `server/src/services/chat.ts` :: branchConversation | messagesToCopy | SELECT lte(createdAt) + INSERT returning | Yes — real DB copy + returns new conversation | ✓ FLOWING |
|
||||
| `server/src/services/chat.ts` :: exportConversation | rows | SELECT + LEFT JOIN agents | Yes — all messages + agent names from DB | ✓ FLOWING |
|
||||
| `ui/src/hooks/useChatSearch.ts` | data | chatApi.searchMessages → GET /companies/:id/messages/search | Yes — enabled when query >= 2 chars, real endpoint | ✓ FLOWING |
|
||||
| `ui/src/hooks/useChatBookmarks.ts` | data | chatApi.getBookmarks → GET /companies/:id/bookmarks | Yes — real endpoint | ✓ FLOWING |
|
||||
| `ui/src/components/ChatPanel.tsx` :: bookmarkedMessageIds | Set<string> | useChatBookmarks(companyId, activeConversationId).data | Yes — rebuilt per-conversation from server data | ✓ FLOWING |
|
||||
| `ui/src/components/ChatPanel.tsx` :: branches | ChatConversation[] | useQuery → chatApi.listBranches → GET /conversations/:id/branches | Yes — real endpoint | ✓ FLOWING |
|
||||
| `ui/src/components/ChatPanel.tsx` :: JSON export | — | handleExport("json") | N/A — UI trigger missing | ✗ DISCONNECTED (no UI entry point) |
|
||||
|
||||
---
|
||||
|
||||
## Behavioral Spot-Checks
|
||||
|
||||
| Behavior | Method | Result | Status |
|
||||
|----------|--------|--------|--------|
|
||||
| All 8 Phase 24 commits exist in git | git log for 8 commit hashes | All 8 found (430bbbb8 through 2b526e78) | ✓ PASS |
|
||||
| Migration journal has 0050/0051/0052 entries | grep in _journal.json | All three tags present | ✓ PASS |
|
||||
| Shared package exports key symbols | node -e require check | ChatMessageSearchResult, ChatBookmark, searchMessagesSchema, branchConversationSchema — all FOUND | ✓ PASS |
|
||||
| searchMessages has real FTS WHERE clause | grep in service | `"content_search" @@ plainto_tsquery` with ts_rank ORDER BY found | ✓ PASS |
|
||||
| export route sets Content-Disposition header | grep in routes | Line 273: res.setHeader("Content-Disposition", ...) confirmed | ✓ PASS |
|
||||
| JSON export has UI trigger in ChatPanel | grep for handleExport("json") | Only markdown button at line 261; no JSON trigger | ✗ FAIL |
|
||||
|
||||
---
|
||||
|
||||
## Requirements Coverage
|
||||
|
||||
| Requirement | Source Plan(s) | Description | Status | Evidence |
|
||||
|-------------|---------------|-------------|--------|----------|
|
||||
| CHAT-07 | 00, 01, 02, 03 | Full-text search across all conversations | ✓ SATISFIED | tsvector GIN index, plainto_tsquery service, ChatSearchDialog, Cmd+K integration all wired |
|
||||
| CHAT-13 | 00, 01, 02, 03 | Message bookmarks: mark important messages | ✓ SATISFIED | ChatMessageBookmark on every message, toggleBookmark service+route, ChatBookmarkList navigation |
|
||||
| CHAT-14 | 01, 03 | Conversation branching on edit | ✓ SATISFIED | branchConversation service, POST /branch route, handleEdit branch-on-edit logic, ChatBranchSelector |
|
||||
| HIST-04 | 00, 01, 02, 03 | Conversation export as Markdown or JSON | ✗ BLOCKED | Server and API client support both formats; ChatPanel only exposes Markdown button — JSON has no UI trigger |
|
||||
| PERF-04 | 01 | FTS returns results <500ms across 10,000+ messages | ? NEEDS HUMAN | GIN index in place; ts_rank ordering correct; runtime perf requires load test or browser timing |
|
||||
|
||||
### Phantom Requirement IDs in Plan Frontmatter
|
||||
|
||||
The following requirement IDs appear in plan frontmatter (`24-00-PLAN.md` through `24-03-PLAN.md`) but **do not exist** in `REQUIREMENTS.md`:
|
||||
|
||||
- `HIST-07`, `HIST-08`, `HIST-09`, `HIST-10`, `HIST-11`, `HIST-12`
|
||||
|
||||
`REQUIREMENTS.md` defines only HIST-01 through HIST-06. These IDs were invented in the plan documents but have no corresponding requirement definitions. The authoritative requirement set for Phase 24 per ROADMAP.md is: **CHAT-07, CHAT-13, CHAT-14, HIST-04, PERF-04** — all of which are accounted for above.
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns Found
|
||||
|
||||
| File | Line | Pattern | Severity | Impact |
|
||||
|------|------|---------|----------|--------|
|
||||
| `ui/src/components/ChatMessageActions.tsx` | 37, 71 | `messageId=""` and `conversationId=""` passed to `ChatMessageBookmark` | ℹ️ Info | Not a functional stub — `ChatMessageBookmark` does not use these props in its body (destructures only `isBookmarked` and `onToggle`); real messageId flows via `onToggle` callback. Props are dead interface noise, not a bug. |
|
||||
| `ui/src/components/ChatMessageList.tsx` | (comment) | `// TODO: if message not found, best-effort only` | ℹ️ Info | Intentional — plan specified this as acceptable for scroll-to on unpaginated messages. Does not block any success criterion. |
|
||||
|
||||
No blocking stubs found. All core implementations contain real DB queries, real HTTP calls, and real state management.
|
||||
|
||||
---
|
||||
|
||||
## Human Verification Required
|
||||
|
||||
### 1. Full-text search round-trip with scroll-to-message
|
||||
|
||||
**Test:** Press Cmd+K in the Nexus UI. Select "Search chat messages". Type a search term that exists in a past conversation message. Verify results appear with conversation title and message snippet. Click a result.
|
||||
**Expected:** ChatSearchDialog closes; the chat panel switches to the target conversation; the virtualizer scrolls to the target message (centered in view) within ~500ms total.
|
||||
**Why human:** Keyboard shortcut dispatch, dialog rendering, virtualizer scroll, and sub-500ms timing cannot be verified without a running browser session.
|
||||
|
||||
### 2. Bookmark persistence across page reload
|
||||
|
||||
**Test:** Open a conversation. Hover a message. Click the bookmark icon (should fill solid). Reload the page. Return to the same conversation and hover the same message.
|
||||
**Expected:** The bookmark icon is still filled, confirming the DB write via POST /conversations/:id/bookmarks persisted and the GET /companies/:id/bookmarks query on reload returns it.
|
||||
**Why human:** Requires live server, DB write, and page reload cycle.
|
||||
|
||||
### 3. Branch-on-edit creates visible branch in sidebar
|
||||
|
||||
**Test:** In a conversation with at least two messages (one user, one assistant reply), click edit on the user message, change the text, and submit.
|
||||
**Expected:** A new branch conversation appears in the sidebar with a GitBranch icon and pl-4 indent under the original. The branch selector bar appears above the message list. Clicking "Original" in the branch selector switches back to the original conversation.
|
||||
**Why human:** Requires an active agent session, streaming, and visual sidebar inspection.
|
||||
|
||||
### 4. Markdown export produces correct agent names (not UUIDs)
|
||||
|
||||
**Test:** In a conversation that involved a named agent, click the Download icon in the chat header. Open the downloaded .md file.
|
||||
**Expected:** The file is named `<title-slug>-<date>.md`; assistant messages show the agent's name (e.g. "Brainstormer") not a UUID; user messages show "You"; format matches `**Speaker** (timestamp)\ncontent\n\n---`.
|
||||
**Why human:** Requires browser file download and manual inspection of content.
|
||||
|
||||
---
|
||||
|
||||
## Gaps Summary
|
||||
|
||||
**1 gap blocks full goal achievement:**
|
||||
|
||||
**JSON export has no UI trigger (Success Criterion 4, HIST-04)**
|
||||
|
||||
The complete JSON export pipeline exists — the server service (`exportConversation`), the Express route (`GET /conversations/:id/export?format=json`), and the API client method (`chatApi.exportConversation(id, "json")`) all work correctly. The `handleExport` callback in ChatPanel also accepts "json" as a format. However, `ChatPanel.tsx` only renders one export button (line 261) that hardcodes `handleExport("markdown")`. No UI element triggers `handleExport("json")`.
|
||||
|
||||
**Fix required:** Add a second export button (or a dropdown with two options) in the ChatPanel header that calls `handleExport("json")`. The server, service, route, API client, and handler are all already correct — only the UI trigger is missing.
|
||||
|
||||
---
|
||||
|
||||
_Verified: 2026-04-01_
|
||||
_Verifier: Claude (gsd-verifier)_
|
||||
Loading…
Add table
Reference in a new issue