@prmichaelsen/acp-visualizer 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/package.json +8 -10
  2. package/src/components/ExtraFieldsBadge.tsx +1 -1
  3. package/src/components/FilterBar.tsx +1 -1
  4. package/src/components/Header.tsx +1 -1
  5. package/src/components/MilestoneTable.tsx +1 -1
  6. package/src/components/MilestoneTree.tsx +2 -2
  7. package/src/components/StatusBadge.tsx +1 -1
  8. package/src/components/StatusDot.tsx +1 -1
  9. package/src/components/TaskList.tsx +1 -1
  10. package/src/routes/__root.tsx +5 -5
  11. package/src/routes/api/watch.ts +1 -1
  12. package/src/routes/index.tsx +2 -2
  13. package/src/routes/milestones.tsx +7 -7
  14. package/src/routes/search.tsx +4 -4
  15. package/src/routes/tasks.tsx +3 -3
  16. package/src/services/progress-database.service.ts +3 -3
  17. package/agent/commands/acp.clarification-address.md +0 -417
  18. package/agent/commands/acp.clarification-capture.md +0 -386
  19. package/agent/commands/acp.clarification-create.md +0 -437
  20. package/agent/commands/acp.clarifications-research.md +0 -326
  21. package/agent/commands/acp.command-create.md +0 -432
  22. package/agent/commands/acp.design-create.md +0 -286
  23. package/agent/commands/acp.design-reference.md +0 -355
  24. package/agent/commands/acp.handoff.md +0 -270
  25. package/agent/commands/acp.index.md +0 -423
  26. package/agent/commands/acp.init.md +0 -546
  27. package/agent/commands/acp.package-create.md +0 -895
  28. package/agent/commands/acp.package-info.md +0 -212
  29. package/agent/commands/acp.package-install.md +0 -539
  30. package/agent/commands/acp.package-list.md +0 -280
  31. package/agent/commands/acp.package-publish.md +0 -541
  32. package/agent/commands/acp.package-remove.md +0 -293
  33. package/agent/commands/acp.package-search.md +0 -307
  34. package/agent/commands/acp.package-update.md +0 -361
  35. package/agent/commands/acp.package-validate.md +0 -540
  36. package/agent/commands/acp.pattern-create.md +0 -386
  37. package/agent/commands/acp.plan.md +0 -587
  38. package/agent/commands/acp.proceed.md +0 -882
  39. package/agent/commands/acp.project-create.md +0 -675
  40. package/agent/commands/acp.project-info.md +0 -312
  41. package/agent/commands/acp.project-list.md +0 -226
  42. package/agent/commands/acp.project-remove.md +0 -379
  43. package/agent/commands/acp.project-set.md +0 -227
  44. package/agent/commands/acp.project-update.md +0 -307
  45. package/agent/commands/acp.projects-restore.md +0 -228
  46. package/agent/commands/acp.projects-sync.md +0 -347
  47. package/agent/commands/acp.report.md +0 -407
  48. package/agent/commands/acp.resume.md +0 -239
  49. package/agent/commands/acp.sessions.md +0 -301
  50. package/agent/commands/acp.status.md +0 -293
  51. package/agent/commands/acp.sync.md +0 -364
  52. package/agent/commands/acp.task-create.md +0 -500
  53. package/agent/commands/acp.update.md +0 -302
  54. package/agent/commands/acp.validate.md +0 -466
  55. package/agent/commands/acp.version-check-for-updates.md +0 -276
  56. package/agent/commands/acp.version-check.md +0 -191
  57. package/agent/commands/acp.version-update.md +0 -289
  58. package/agent/commands/command.template.md +0 -339
  59. package/agent/commands/git.commit.md +0 -526
  60. package/agent/commands/git.init.md +0 -514
  61. package/agent/commands/tanstack-cloudflare.deploy.md +0 -272
  62. package/agent/commands/tanstack-cloudflare.tail.md +0 -275
  63. package/agent/design/.gitkeep +0 -0
  64. package/agent/design/design.template.md +0 -154
  65. package/agent/design/local.dashboard-layout-routing.md +0 -288
  66. package/agent/design/local.data-model-yaml-parsing.md +0 -310
  67. package/agent/design/local.search-filtering.md +0 -331
  68. package/agent/design/local.server-api-auto-refresh.md +0 -235
  69. package/agent/design/local.table-tree-views.md +0 -299
  70. package/agent/design/local.visualizer-requirements.md +0 -349
  71. package/agent/design/requirements.template.md +0 -387
  72. package/agent/index/.gitkeep +0 -0
  73. package/agent/index/acp.core.yaml +0 -137
  74. package/agent/index/local.main.template.yaml +0 -37
  75. package/agent/manifest.template.yaml +0 -13
  76. package/agent/manifest.yaml +0 -302
  77. package/agent/milestones/.gitkeep +0 -0
  78. package/agent/milestones/milestone-1-project-scaffold-data-pipeline.md +0 -67
  79. package/agent/milestones/milestone-1-{title}.template.md +0 -206
  80. package/agent/milestones/milestone-2-dashboard-views-interaction.md +0 -79
  81. package/agent/package.template.yaml +0 -86
  82. package/agent/patterns/.gitkeep +0 -0
  83. package/agent/patterns/bootstrap.template.md +0 -1237
  84. package/agent/patterns/pattern.template.md +0 -382
  85. package/agent/patterns/tanstack-cloudflare.acl-permissions.md +0 -332
  86. package/agent/patterns/tanstack-cloudflare.action-bar-item.md +0 -416
  87. package/agent/patterns/tanstack-cloudflare.api-route-handlers.md +0 -401
  88. package/agent/patterns/tanstack-cloudflare.auth-session-management.md +0 -387
  89. package/agent/patterns/tanstack-cloudflare.card-and-list.md +0 -271
  90. package/agent/patterns/tanstack-cloudflare.chat-engine.md +0 -353
  91. package/agent/patterns/tanstack-cloudflare.confirmation-tokens.md +0 -346
  92. package/agent/patterns/tanstack-cloudflare.durable-objects-websocket.md +0 -516
  93. package/agent/patterns/tanstack-cloudflare.email-service.md +0 -431
  94. package/agent/patterns/tanstack-cloudflare.expander.md +0 -98
  95. package/agent/patterns/tanstack-cloudflare.fcm-push.md +0 -115
  96. package/agent/patterns/tanstack-cloudflare.firebase-anonymous-sessions.md +0 -441
  97. package/agent/patterns/tanstack-cloudflare.firebase-auth.md +0 -348
  98. package/agent/patterns/tanstack-cloudflare.firebase-firestore.md +0 -550
  99. package/agent/patterns/tanstack-cloudflare.firebase-storage.md +0 -369
  100. package/agent/patterns/tanstack-cloudflare.form-controls.md +0 -145
  101. package/agent/patterns/tanstack-cloudflare.global-search-context.md +0 -93
  102. package/agent/patterns/tanstack-cloudflare.image-carousel.md +0 -126
  103. package/agent/patterns/tanstack-cloudflare.library-services.md +0 -553
  104. package/agent/patterns/tanstack-cloudflare.lightbox.md +0 -169
  105. package/agent/patterns/tanstack-cloudflare.markdown-content.md +0 -115
  106. package/agent/patterns/tanstack-cloudflare.mention-suggestions.md +0 -98
  107. package/agent/patterns/tanstack-cloudflare.modal.md +0 -156
  108. package/agent/patterns/tanstack-cloudflare.nextjs-to-tanstack-routing.md +0 -461
  109. package/agent/patterns/tanstack-cloudflare.notifications-engine.md +0 -151
  110. package/agent/patterns/tanstack-cloudflare.oauth-token-refresh.md +0 -90
  111. package/agent/patterns/tanstack-cloudflare.og-metadata.md +0 -296
  112. package/agent/patterns/tanstack-cloudflare.pagination.md +0 -442
  113. package/agent/patterns/tanstack-cloudflare.pill-input.md +0 -220
  114. package/agent/patterns/tanstack-cloudflare.provider-adapter.md +0 -401
  115. package/agent/patterns/tanstack-cloudflare.rate-limiting.md +0 -323
  116. package/agent/patterns/tanstack-cloudflare.scheduled-tasks.md +0 -338
  117. package/agent/patterns/tanstack-cloudflare.searchable-settings.md +0 -375
  118. package/agent/patterns/tanstack-cloudflare.slide-over.md +0 -129
  119. package/agent/patterns/tanstack-cloudflare.ssr-preload.md +0 -571
  120. package/agent/patterns/tanstack-cloudflare.third-party-api-integration.md +0 -508
  121. package/agent/patterns/tanstack-cloudflare.toast-system.md +0 -142
  122. package/agent/patterns/tanstack-cloudflare.unified-header.md +0 -280
  123. package/agent/patterns/tanstack-cloudflare.user-scoped-collections.md +0 -628
  124. package/agent/patterns/tanstack-cloudflare.websocket-manager.md +0 -237
  125. package/agent/patterns/tanstack-cloudflare.wrangler-configuration.md +0 -358
  126. package/agent/patterns/tanstack-cloudflare.zod-schema-validation.md +0 -336
  127. package/agent/progress.template.yaml +0 -161
  128. package/agent/progress.yaml +0 -145
  129. package/agent/schemas/package.schema.yaml +0 -276
  130. package/agent/scripts/acp.common.sh +0 -1781
  131. package/agent/scripts/acp.install.sh +0 -333
  132. package/agent/scripts/acp.package-create.sh +0 -924
  133. package/agent/scripts/acp.package-info.sh +0 -288
  134. package/agent/scripts/acp.package-install.sh +0 -893
  135. package/agent/scripts/acp.package-list.sh +0 -311
  136. package/agent/scripts/acp.package-publish.sh +0 -420
  137. package/agent/scripts/acp.package-remove.sh +0 -348
  138. package/agent/scripts/acp.package-search.sh +0 -156
  139. package/agent/scripts/acp.package-update.sh +0 -517
  140. package/agent/scripts/acp.package-validate.sh +0 -1018
  141. package/agent/scripts/acp.uninstall.sh +0 -85
  142. package/agent/scripts/acp.version-check-for-updates.sh +0 -98
  143. package/agent/scripts/acp.version-check.sh +0 -47
  144. package/agent/scripts/acp.version-update.sh +0 -176
  145. package/agent/scripts/acp.yaml-parser.sh +0 -985
  146. package/agent/scripts/acp.yaml-validate.sh +0 -205
  147. package/agent/tasks/.gitkeep +0 -0
  148. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-1-initialize-tanstack-start-project.md +0 -210
  149. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-2-implement-data-model-yaml-parser.md +0 -294
  150. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-3-build-server-api-data-loading.md +0 -193
  151. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-4-add-auto-refresh-sse.md +0 -262
  152. package/agent/tasks/milestone-2-dashboard-views-interaction/task-10-polish-integration-testing.md +0 -156
  153. package/agent/tasks/milestone-2-dashboard-views-interaction/task-5-build-dashboard-layout-routing.md +0 -178
  154. package/agent/tasks/milestone-2-dashboard-views-interaction/task-6-build-overview-page.md +0 -141
  155. package/agent/tasks/milestone-2-dashboard-views-interaction/task-7-implement-milestone-table-view.md +0 -153
  156. package/agent/tasks/milestone-2-dashboard-views-interaction/task-8-implement-milestone-tree-view.md +0 -174
  157. package/agent/tasks/milestone-2-dashboard-views-interaction/task-9-implement-search-filtering.md +0 -233
  158. package/agent/tasks/task-1-{title}.template.md +0 -244
  159. package/vitest.config.ts +0 -27
@@ -1,331 +0,0 @@
1
- # Search & Filtering
2
-
3
- **Concept**: Fuse.js-powered fuzzy search and status-based filtering across milestones and tasks
4
- **Created**: 2026-03-14
5
- **Status**: Design Specification
6
-
7
- ---
8
-
9
- ## Overview
10
-
11
- Defines the search and filtering system that lets users quickly find milestones and tasks by name/content (via fuse.js fuzzy search) and narrow results by status. Search and filter state is global — it persists across view switches (table ↔ tree) and is accessible from the sidebar.
12
-
13
- ---
14
-
15
- ## Problem Statement
16
-
17
- With 10+ milestones and 50+ tasks, users need to:
18
- - Find a specific milestone or task by name or keyword
19
- - Focus on only in-progress items without scrolling past completed work
20
- - Combine search and filter (e.g., "auth" tasks that are in-progress)
21
-
22
- Without search/filter, users are back to mentally parsing a long list — defeating the purpose of a visual dashboard.
23
-
24
- ---
25
-
26
- ## Solution
27
-
28
- 1. **Fuse.js search index** built from milestones and tasks on data load
29
- 2. **Status filter** (all / not_started / in_progress / completed) applied as a pre-filter
30
- 3. **FilterContext** React context providing shared state across all views
31
- 4. **SearchBar** component in sidebar for global access
32
- 5. **FilterBar** component on milestone/task pages for quick status toggles
33
-
34
- ---
35
-
36
- ## Implementation
37
-
38
- ### Search Index
39
-
40
- ```typescript
41
- // app/lib/search.ts
42
-
43
- import Fuse from 'fuse.js';
44
- import type { ProgressData, Milestone, Task } from './types';
45
-
46
- export type SearchResult = {
47
- type: 'milestone' | 'task';
48
- milestone: Milestone;
49
- task?: Task;
50
- score: number;
51
- };
52
-
53
- export function buildSearchIndex(data: ProgressData) {
54
- const items: Array<{
55
- type: 'milestone' | 'task';
56
- milestone: Milestone;
57
- task?: Task;
58
- name: string;
59
- notes: string;
60
- extra: string; // stringified extra fields for search coverage
61
- }> = [];
62
-
63
- for (const milestone of data.milestones) {
64
- items.push({
65
- type: 'milestone',
66
- milestone,
67
- name: milestone.name,
68
- notes: milestone.notes,
69
- extra: JSON.stringify(milestone.extra),
70
- });
71
-
72
- const tasks = data.tasks[milestone.id] || [];
73
- for (const task of tasks) {
74
- items.push({
75
- type: 'task',
76
- milestone,
77
- task,
78
- name: task.name,
79
- notes: task.notes,
80
- extra: JSON.stringify(task.extra),
81
- });
82
- }
83
- }
84
-
85
- return new Fuse(items, {
86
- keys: [
87
- { name: 'name', weight: 2 },
88
- { name: 'notes', weight: 1 },
89
- { name: 'extra', weight: 0.5 },
90
- ],
91
- threshold: 0.4, // moderate fuzziness
92
- includeScore: true,
93
- ignoreLocation: true, // match anywhere in string
94
- });
95
- }
96
- ```
97
-
98
- ### Filter Context
99
-
100
- ```tsx
101
- // app/lib/filter-context.tsx
102
-
103
- import { createContext, useContext, useState, useMemo } from 'react';
104
- import type { ProgressData, Status } from './types';
105
-
106
- interface FilterState {
107
- status: Status | 'all';
108
- search: string;
109
- }
110
-
111
- interface FilterContextValue {
112
- filters: FilterState;
113
- setStatus: (status: Status | 'all') => void;
114
- setSearch: (query: string) => void;
115
- filteredData: ProgressData;
116
- }
117
-
118
- const FilterContext = createContext<FilterContextValue>(null!);
119
-
120
- export function FilterProvider({ data, children }: { data: ProgressData; children: React.ReactNode }) {
121
- const [filters, setFilters] = useState<FilterState>({ status: 'all', search: '' });
122
-
123
- const filteredData = useMemo(() => {
124
- let milestones = data.milestones;
125
- let tasks = { ...data.tasks };
126
-
127
- // Status filter
128
- if (filters.status !== 'all') {
129
- milestones = milestones.filter(m => m.status === filters.status);
130
- tasks = Object.fromEntries(
131
- Object.entries(tasks).map(([id, ts]) => [
132
- id,
133
- ts.filter(t => t.status === filters.status),
134
- ])
135
- );
136
- }
137
-
138
- // Search filter (if query present, intersect with search results)
139
- if (filters.search.trim()) {
140
- const index = buildSearchIndex(data);
141
- const results = index.search(filters.search);
142
- const matchedMilestoneIds = new Set(
143
- results.map(r => r.item.milestone.id)
144
- );
145
- const matchedTaskIds = new Set(
146
- results.filter(r => r.item.task).map(r => r.item.task!.id)
147
- );
148
- milestones = milestones.filter(m => matchedMilestoneIds.has(m.id));
149
- tasks = Object.fromEntries(
150
- Object.entries(tasks).map(([id, ts]) => [
151
- id,
152
- ts.filter(t => matchedTaskIds.has(t.id) || matchedMilestoneIds.has(id)),
153
- ])
154
- );
155
- }
156
-
157
- return { ...data, milestones, tasks };
158
- }, [data, filters]);
159
-
160
- return (
161
- <FilterContext.Provider value={{
162
- filters,
163
- setStatus: (status) => setFilters(f => ({ ...f, status })),
164
- setSearch: (search) => setFilters(f => ({ ...f, search })),
165
- filteredData,
166
- }}>
167
- {children}
168
- </FilterContext.Provider>
169
- );
170
- }
171
-
172
- export const useFilters = () => useContext(FilterContext);
173
- ```
174
-
175
- ### FilterBar Component
176
-
177
- Status toggle buttons displayed above milestone/task views:
178
-
179
- ```tsx
180
- // app/components/FilterBar.tsx
181
-
182
- const statusOptions: Array<{ value: Status | 'all'; label: string }> = [
183
- { value: 'all', label: 'All' },
184
- { value: 'in_progress', label: 'In Progress' },
185
- { value: 'not_started', label: 'Not Started' },
186
- { value: 'completed', label: 'Completed' },
187
- ];
188
-
189
- function FilterBar() {
190
- const { filters, setStatus } = useFilters();
191
-
192
- return (
193
- <div className="flex gap-1 mb-4">
194
- {statusOptions.map(opt => (
195
- <button
196
- key={opt.value}
197
- onClick={() => setStatus(opt.value)}
198
- className={`px-3 py-1 text-xs rounded-full border transition-colors ${
199
- filters.status === opt.value
200
- ? 'bg-gray-700 border-gray-600 text-gray-100'
201
- : 'bg-transparent border-gray-800 text-gray-500 hover:text-gray-300'
202
- }`}
203
- >
204
- {opt.label}
205
- </button>
206
- ))}
207
- </div>
208
- );
209
- }
210
- ```
211
-
212
- ### SearchBar Component
213
-
214
- Located in the sidebar footer, accessible from any page:
215
-
216
- ```tsx
217
- // app/components/SearchBar.tsx
218
-
219
- function SearchBar() {
220
- const { filters, setSearch } = useFilters();
221
-
222
- return (
223
- <div className="relative">
224
- <SearchIcon className="absolute left-2 top-2 w-4 h-4 text-gray-500" />
225
- <input
226
- type="text"
227
- value={filters.search}
228
- onChange={e => setSearch(e.target.value)}
229
- placeholder="Search..."
230
- className="w-full bg-gray-900 border border-gray-800 rounded-md pl-8 pr-3 py-1.5 text-sm text-gray-200 placeholder-gray-600 focus:outline-none focus:border-gray-600"
231
- />
232
- </div>
233
- );
234
- }
235
- ```
236
-
237
- ### Search Results Page
238
-
239
- Dedicated `/search` route for viewing detailed search results when triggered from sidebar:
240
-
241
- ```tsx
242
- // app/routes/search.tsx
243
-
244
- function SearchPage() {
245
- const { filteredData, filters } = useFilters();
246
-
247
- if (!filters.search.trim()) {
248
- return <p className="text-gray-500 text-sm">Type to search milestones and tasks</p>;
249
- }
250
-
251
- return (
252
- <div className="space-y-4">
253
- <h2 className="text-sm text-gray-400">
254
- Results for "{filters.search}"
255
- </h2>
256
- {/* Grouped by milestones, then tasks */}
257
- {filteredData.milestones.map(m => (
258
- <SearchResultCard key={m.id} milestone={m} tasks={filteredData.tasks[m.id]} />
259
- ))}
260
- </div>
261
- );
262
- }
263
- ```
264
-
265
- ---
266
-
267
- ## Benefits
268
-
269
- - **Fast**: Fuse.js search is client-side, instant feedback
270
- - **Fuzzy**: Typo-tolerant matching (threshold 0.4)
271
- - **Combined**: Search + status filter work together naturally
272
- - **Global**: Shared state persists across view switches
273
- - **Extra fields searchable**: Agent-added properties are included in the search index
274
-
275
- ---
276
-
277
- ## Trade-offs
278
-
279
- - **Client-side only**: Search index is rebuilt on each data load (acceptable for <100KB data)
280
- - **No server-side search**: All data is loaded upfront (fine for single-project P0)
281
- - **Fuse.js bundle size**: ~15KB gzipped (acceptable for admin dashboard)
282
-
283
- ---
284
-
285
- ## Applicable Patterns
286
-
287
- | Pattern | How It Applies |
288
- |---------|----------------|
289
- | [`tanstack-cloudflare.global-search-context`](../patterns/tanstack-cloudflare.global-search-context.md) | Adopt the lightweight pub/sub `useRef` store for cross-component search/filter state instead of heavy React Context re-renders. `useGlobalSearch(key)` returns `[value, setValue]` and only re-renders subscribers of that key. Key convention: `milestones:search`, `milestones:status`. Wrap app in `<GlobalSearchProvider>` in root layout. This replaces the FilterContext approach in the implementation section — the GlobalSearchContext pattern is more performant for string-based filter state. |
290
- | [`tanstack-cloudflare.pill-input`](../patterns/tanstack-cloudflare.pill-input.md) | Provides the proven fuse.js configuration: `threshold: 0.4`, `ignoreLocation: true`, keyboard navigation (arrows, Enter, Escape). The PillInput pattern's fuzzy matching config should be adopted for the search index. Also demonstrates combining predefined options with custom entries — applicable to filter presets. |
291
- | [`tanstack-cloudflare.searchable-settings`](../patterns/tanstack-cloudflare.searchable-settings.md) | The registry-based AND-logic search pattern applies to multi-field filtering. Each searchable item has `name`, `description`, `keywords` — map to milestone/task names, notes, and extra field values. Hash-based scroll navigation could enable direct linking to search results. |
292
- | [`tanstack-cloudflare.form-controls`](../patterns/tanstack-cloudflare.form-controls.md) | ToggleSwitch component (iOS-style, ARIA-accessible) for boolean filter controls. Could be used for "show completed" toggle. Fully keyboard-accessible with proper ARIA labels. |
293
- | [`tanstack-cloudflare.unified-header`](../patterns/tanstack-cloudflare.unified-header.md) | FilterTabs component (inline pill-style filter controls): `flex gap-1 mb-4 p-1 bg-gray-800/50 rounded-lg`. Active button gets gradient background, inactive gets gray text with hover. Adopt this exact styling for the status filter bar rather than custom implementation. |
294
-
295
- ---
296
-
297
- ## Dependencies
298
-
299
- - `fuse.js` — fuzzy search library
300
- - React context API (or GlobalSearchContext pub/sub — see Applicable Patterns)
301
-
302
- ---
303
-
304
- ## Testing Strategy
305
-
306
- - **Unit tests**: `buildSearchIndex` returns correct results for known queries
307
- - **Fuzzy matching**: Typos still find results (e.g., "infrastrcture" → "infrastructure")
308
- - **Filter tests**: Status filter correctly narrows milestone/task lists
309
- - **Combined tests**: Search + filter intersection works correctly
310
- - **Empty states**: No results shows appropriate message
311
-
312
- ---
313
-
314
- ## Migration Path
315
-
316
- N/A — greenfield project.
317
-
318
- ---
319
-
320
- ## Future Considerations
321
-
322
- - **Keyboard shortcut**: `Cmd+K` to focus search bar
323
- - **Search history**: Recent searches stored in localStorage
324
- - **Advanced filters**: Filter by date range, estimated hours, completion percentage
325
- - **P1: Recent work search**: Include WorkEntry items in search index
326
-
327
- ---
328
-
329
- **Status**: Design Specification
330
- **Recommendation**: Implement alongside views — search/filter enhances all view components
331
- **Related Documents**: local.visualizer-requirements.md, local.table-tree-views.md, tanstack-cloudflare.global-search-context, tanstack-cloudflare.pill-input, tanstack-cloudflare.searchable-settings, tanstack-cloudflare.form-controls, tanstack-cloudflare.unified-header
@@ -1,235 +0,0 @@
1
- # Server API & Auto-Refresh
2
-
3
- **Concept**: TanStack Start server routes for loading progress.yaml and SSE-based auto-refresh
4
- **Created**: 2026-03-14
5
- **Status**: Design Specification
6
-
7
- ---
8
-
9
- ## Overview
10
-
11
- Defines the server-side architecture for loading `progress.yaml` from the local filesystem and pushing updates to the browser in real time when the file changes. Uses TanStack Start server functions and Server-Sent Events (SSE) for a simple, reliable auto-refresh mechanism.
12
-
13
- ---
14
-
15
- ## Problem Statement
16
-
17
- The visualizer needs to:
18
- - Read `progress.yaml` from a configurable filesystem path
19
- - Serve parsed data to the React frontend
20
- - Automatically refresh the dashboard when agents modify `progress.yaml` during work
21
- - Handle file-not-found and parse errors gracefully
22
-
23
- A simple "refresh the page" approach is insufficient — agents may update progress.yaml dozens of times during a session, and the dashboard should reflect changes without manual intervention.
24
-
25
- ---
26
-
27
- ## Solution
28
-
29
- 1. **Server function**: TanStack Start `createServerFn` that reads and parses `progress.yaml`
30
- 2. **SSE endpoint**: Watches the file for changes and pushes notifications to connected clients
31
- 3. **Client hook**: `useAutoRefresh()` React hook that listens for SSE events and triggers re-fetch
32
-
33
- ---
34
-
35
- ## Implementation
36
-
37
- ### Configuration
38
-
39
- The path to `progress.yaml` is resolved from (in order):
40
- 1. `PROGRESS_YAML_PATH` environment variable (absolute path)
41
- 2. CLI argument: `npm run dev -- --progress /path/to/progress.yaml`
42
- 3. Default: `./agent/progress.yaml` (relative to cwd)
43
-
44
- ```typescript
45
- // app/lib/config.ts
46
-
47
- export function getProgressYamlPath(): string {
48
- return (
49
- process.env.PROGRESS_YAML_PATH ||
50
- process.argv.find((_, i, a) => a[i - 1] === '--progress') ||
51
- './agent/progress.yaml'
52
- );
53
- }
54
- ```
55
-
56
- ### Server Function
57
-
58
- ```typescript
59
- // app/lib/data-source.ts
60
-
61
- import { createServerFn } from '@tanstack/react-start';
62
- import { readFileSync } from 'fs';
63
- import { parseProgressYaml } from './yaml-loader';
64
- import { getProgressYamlPath } from './config';
65
-
66
- export const getProgressData = createServerFn({ method: 'GET' })
67
- .handler(async () => {
68
- const filePath = getProgressYamlPath();
69
- const raw = readFileSync(filePath, 'utf-8');
70
- return parseProgressYaml(raw);
71
- });
72
- ```
73
-
74
- ### File Watcher & SSE
75
-
76
- ```typescript
77
- // server/routes/api/watch.ts
78
-
79
- import { watch } from 'fs';
80
- import { getProgressYamlPath } from '../../app/lib/config';
81
-
82
- export function createFileWatcher() {
83
- const filePath = getProgressYamlPath();
84
- const clients = new Set<ReadableStreamDefaultController>();
85
-
86
- watch(filePath, (eventType) => {
87
- if (eventType === 'change') {
88
- for (const controller of clients) {
89
- controller.enqueue(`data: refresh\n\n`);
90
- }
91
- }
92
- });
93
-
94
- return {
95
- addClient(controller: ReadableStreamDefaultController) {
96
- clients.add(controller);
97
- },
98
- removeClient(controller: ReadableStreamDefaultController) {
99
- clients.delete(controller);
100
- },
101
- };
102
- }
103
- ```
104
-
105
- ### SSE API Route
106
-
107
- ```typescript
108
- // app/routes/api/watch.ts
109
-
110
- import { createAPIFileRoute } from '@tanstack/react-start/api';
111
-
112
- let watcher: ReturnType<typeof createFileWatcher> | null = null;
113
-
114
- export const APIRoute = createAPIFileRoute('/api/watch')({
115
- GET: async () => {
116
- if (!watcher) {
117
- watcher = createFileWatcher();
118
- }
119
-
120
- const stream = new ReadableStream({
121
- start(controller) {
122
- watcher!.addClient(controller);
123
- },
124
- cancel(controller) {
125
- watcher!.removeClient(controller);
126
- },
127
- });
128
-
129
- return new Response(stream, {
130
- headers: {
131
- 'Content-Type': 'text/event-stream',
132
- 'Cache-Control': 'no-cache',
133
- 'Connection': 'keep-alive',
134
- },
135
- });
136
- },
137
- });
138
- ```
139
-
140
- ### Client Hook
141
-
142
- ```typescript
143
- // app/lib/useAutoRefresh.ts
144
-
145
- import { useRouter } from '@tanstack/react-router';
146
- import { useEffect } from 'react';
147
-
148
- export function useAutoRefresh() {
149
- const router = useRouter();
150
-
151
- useEffect(() => {
152
- const eventSource = new EventSource('/api/watch');
153
-
154
- eventSource.onmessage = () => {
155
- router.invalidate();
156
- };
157
-
158
- eventSource.onerror = () => {
159
- // Reconnect handled automatically by EventSource
160
- };
161
-
162
- return () => eventSource.close();
163
- }, [router]);
164
- }
165
- ```
166
-
167
- ### Error Handling
168
-
169
- - **File not found**: Server function returns a structured error object with `{ error: 'FILE_NOT_FOUND', path }`. Frontend shows a configuration prompt.
170
- - **Parse error**: Returns `{ error: 'PARSE_ERROR', message }`. Frontend shows the error with the raw YAML path.
171
- - **File watcher failure**: Falls back to manual refresh. Log warning to console.
172
-
173
- ---
174
-
175
- ## Benefits
176
-
177
- - **Real-time updates**: Dashboard reflects changes within ~100ms of file save
178
- - **Low overhead**: SSE is lightweight compared to WebSocket for one-way push
179
- - **Simple client**: `EventSource` API handles reconnection automatically
180
- - **No polling**: File watcher is event-driven, not interval-based
181
-
182
- ---
183
-
184
- ## Trade-offs
185
-
186
- - **Local only**: `fs.watch` only works for local development (P1 GitHub source uses polling instead)
187
- - **Single file**: Watches one progress.yaml path — no multi-project support in P0
188
- - **SSE limitations**: One-way communication only (sufficient for refresh notifications)
189
-
190
- ---
191
-
192
- ## Applicable Patterns
193
-
194
- | Pattern | How It Applies |
195
- |---------|----------------|
196
- | [`tanstack-cloudflare.ssr-preload`](../patterns/tanstack-cloudflare.ssr-preload.md) | Progress data should be loaded server-side via `beforeLoad` (not `loader`), passed through route context, and accessed via `Route.useRouteContext()`. Components initialize state with SSR data and skip client fetch if present. Error handling must be graceful — `try/catch` with empty-data fallback, never fail the page load. |
197
- | [`tanstack-cloudflare.api-route-handlers`](../patterns/tanstack-cloudflare.api-route-handlers.md) | The SSE `/api/watch` endpoint and any future REST endpoints should use `createFileRoute` with `server.handlers` returning Web Standard `Response` objects. Consistent error response format: `{ error, message }`. No auth needed for P0 (local dev tool). |
198
- | [`tanstack-cloudflare.library-services`](../patterns/tanstack-cloudflare.library-services.md) | File reading and YAML parsing must go through a `ProgressDatabaseService` (server-side) — never call `readFileSync` directly in routes or `beforeLoad`. The service handles path resolution, file reading, parsing, and error wrapping. |
199
- | [`tanstack-cloudflare.websocket-manager`](../patterns/tanstack-cloudflare.websocket-manager.md) | While we use SSE (not WebSocket) for P0, the reconnection and lifecycle patterns apply: exponential backoff on connection loss, page visibility recovery (reconnect on tab focus), and clean teardown in `useEffect` cleanup. `EventSource` handles reconnection natively but visibility recovery should still be explicit. |
200
-
201
- ---
202
-
203
- ## Dependencies
204
-
205
- - Node.js `fs` module (built-in)
206
- - TanStack Start server functions
207
- - No additional npm packages needed
208
-
209
- ---
210
-
211
- ## Testing Strategy
212
-
213
- - **Unit tests**: `getProgressYamlPath()` resolution order
214
- - **Integration tests**: Server function reads real YAML file, returns typed data
215
- - **Manual testing**: Modify progress.yaml while dashboard is open, verify auto-refresh
216
-
217
- ---
218
-
219
- ## Migration Path
220
-
221
- N/A — greenfield project.
222
-
223
- ---
224
-
225
- ## Future Considerations
226
-
227
- - **P1: GitHub remote source** — Replace `readFileSync` with GitHub API fetch, replace SSE with configurable polling interval
228
- - **P1: Multi-project** — Watch multiple progress.yaml paths, route by project
229
- - **WebSocket upgrade** — If bidirectional communication needed in future (unlikely for read-only dashboard)
230
-
231
- ---
232
-
233
- **Status**: Design Specification
234
- **Recommendation**: Implement after data model — provides the data pipeline for all views
235
- **Related Documents**: local.visualizer-requirements.md, local.data-model-yaml-parsing.md, tanstack-cloudflare.ssr-preload, tanstack-cloudflare.api-route-handlers, tanstack-cloudflare.library-services, tanstack-cloudflare.websocket-manager