@prmichaelsen/acp-visualizer 0.1.0
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.
- package/README.md +68 -0
- package/agent/commands/acp.clarification-address.md +417 -0
- package/agent/commands/acp.clarification-capture.md +386 -0
- package/agent/commands/acp.clarification-create.md +437 -0
- package/agent/commands/acp.clarifications-research.md +326 -0
- package/agent/commands/acp.command-create.md +432 -0
- package/agent/commands/acp.design-create.md +286 -0
- package/agent/commands/acp.design-reference.md +355 -0
- package/agent/commands/acp.handoff.md +270 -0
- package/agent/commands/acp.index.md +423 -0
- package/agent/commands/acp.init.md +546 -0
- package/agent/commands/acp.package-create.md +895 -0
- package/agent/commands/acp.package-info.md +212 -0
- package/agent/commands/acp.package-install.md +539 -0
- package/agent/commands/acp.package-list.md +280 -0
- package/agent/commands/acp.package-publish.md +541 -0
- package/agent/commands/acp.package-remove.md +293 -0
- package/agent/commands/acp.package-search.md +307 -0
- package/agent/commands/acp.package-update.md +361 -0
- package/agent/commands/acp.package-validate.md +540 -0
- package/agent/commands/acp.pattern-create.md +386 -0
- package/agent/commands/acp.plan.md +587 -0
- package/agent/commands/acp.proceed.md +882 -0
- package/agent/commands/acp.project-create.md +675 -0
- package/agent/commands/acp.project-info.md +312 -0
- package/agent/commands/acp.project-list.md +226 -0
- package/agent/commands/acp.project-remove.md +379 -0
- package/agent/commands/acp.project-set.md +227 -0
- package/agent/commands/acp.project-update.md +307 -0
- package/agent/commands/acp.projects-restore.md +228 -0
- package/agent/commands/acp.projects-sync.md +347 -0
- package/agent/commands/acp.report.md +407 -0
- package/agent/commands/acp.resume.md +239 -0
- package/agent/commands/acp.sessions.md +301 -0
- package/agent/commands/acp.status.md +293 -0
- package/agent/commands/acp.sync.md +364 -0
- package/agent/commands/acp.task-create.md +500 -0
- package/agent/commands/acp.update.md +302 -0
- package/agent/commands/acp.validate.md +466 -0
- package/agent/commands/acp.version-check-for-updates.md +276 -0
- package/agent/commands/acp.version-check.md +191 -0
- package/agent/commands/acp.version-update.md +289 -0
- package/agent/commands/command.template.md +339 -0
- package/agent/commands/git.commit.md +526 -0
- package/agent/commands/git.init.md +514 -0
- package/agent/commands/tanstack-cloudflare.deploy.md +272 -0
- package/agent/commands/tanstack-cloudflare.tail.md +275 -0
- package/agent/design/.gitkeep +0 -0
- package/agent/design/design.template.md +154 -0
- package/agent/design/local.dashboard-layout-routing.md +288 -0
- package/agent/design/local.data-model-yaml-parsing.md +310 -0
- package/agent/design/local.search-filtering.md +331 -0
- package/agent/design/local.server-api-auto-refresh.md +235 -0
- package/agent/design/local.table-tree-views.md +299 -0
- package/agent/design/local.visualizer-requirements.md +349 -0
- package/agent/design/requirements.template.md +387 -0
- package/agent/index/.gitkeep +0 -0
- package/agent/index/acp.core.yaml +137 -0
- package/agent/index/local.main.template.yaml +37 -0
- package/agent/manifest.template.yaml +13 -0
- package/agent/manifest.yaml +302 -0
- package/agent/milestones/.gitkeep +0 -0
- package/agent/milestones/milestone-1-project-scaffold-data-pipeline.md +67 -0
- package/agent/milestones/milestone-1-{title}.template.md +206 -0
- package/agent/milestones/milestone-2-dashboard-views-interaction.md +79 -0
- package/agent/package.template.yaml +86 -0
- package/agent/patterns/.gitkeep +0 -0
- package/agent/patterns/bootstrap.template.md +1237 -0
- package/agent/patterns/pattern.template.md +382 -0
- package/agent/patterns/tanstack-cloudflare.acl-permissions.md +332 -0
- package/agent/patterns/tanstack-cloudflare.action-bar-item.md +416 -0
- package/agent/patterns/tanstack-cloudflare.api-route-handlers.md +401 -0
- package/agent/patterns/tanstack-cloudflare.auth-session-management.md +387 -0
- package/agent/patterns/tanstack-cloudflare.card-and-list.md +271 -0
- package/agent/patterns/tanstack-cloudflare.chat-engine.md +353 -0
- package/agent/patterns/tanstack-cloudflare.confirmation-tokens.md +346 -0
- package/agent/patterns/tanstack-cloudflare.durable-objects-websocket.md +516 -0
- package/agent/patterns/tanstack-cloudflare.email-service.md +431 -0
- package/agent/patterns/tanstack-cloudflare.expander.md +98 -0
- package/agent/patterns/tanstack-cloudflare.fcm-push.md +115 -0
- package/agent/patterns/tanstack-cloudflare.firebase-anonymous-sessions.md +441 -0
- package/agent/patterns/tanstack-cloudflare.firebase-auth.md +348 -0
- package/agent/patterns/tanstack-cloudflare.firebase-firestore.md +550 -0
- package/agent/patterns/tanstack-cloudflare.firebase-storage.md +369 -0
- package/agent/patterns/tanstack-cloudflare.form-controls.md +145 -0
- package/agent/patterns/tanstack-cloudflare.global-search-context.md +93 -0
- package/agent/patterns/tanstack-cloudflare.image-carousel.md +126 -0
- package/agent/patterns/tanstack-cloudflare.library-services.md +553 -0
- package/agent/patterns/tanstack-cloudflare.lightbox.md +169 -0
- package/agent/patterns/tanstack-cloudflare.markdown-content.md +115 -0
- package/agent/patterns/tanstack-cloudflare.mention-suggestions.md +98 -0
- package/agent/patterns/tanstack-cloudflare.modal.md +156 -0
- package/agent/patterns/tanstack-cloudflare.nextjs-to-tanstack-routing.md +461 -0
- package/agent/patterns/tanstack-cloudflare.notifications-engine.md +151 -0
- package/agent/patterns/tanstack-cloudflare.oauth-token-refresh.md +90 -0
- package/agent/patterns/tanstack-cloudflare.og-metadata.md +296 -0
- package/agent/patterns/tanstack-cloudflare.pagination.md +442 -0
- package/agent/patterns/tanstack-cloudflare.pill-input.md +220 -0
- package/agent/patterns/tanstack-cloudflare.provider-adapter.md +401 -0
- package/agent/patterns/tanstack-cloudflare.rate-limiting.md +323 -0
- package/agent/patterns/tanstack-cloudflare.scheduled-tasks.md +338 -0
- package/agent/patterns/tanstack-cloudflare.searchable-settings.md +375 -0
- package/agent/patterns/tanstack-cloudflare.slide-over.md +129 -0
- package/agent/patterns/tanstack-cloudflare.ssr-preload.md +571 -0
- package/agent/patterns/tanstack-cloudflare.third-party-api-integration.md +508 -0
- package/agent/patterns/tanstack-cloudflare.toast-system.md +142 -0
- package/agent/patterns/tanstack-cloudflare.unified-header.md +280 -0
- package/agent/patterns/tanstack-cloudflare.user-scoped-collections.md +628 -0
- package/agent/patterns/tanstack-cloudflare.websocket-manager.md +237 -0
- package/agent/patterns/tanstack-cloudflare.wrangler-configuration.md +358 -0
- package/agent/patterns/tanstack-cloudflare.zod-schema-validation.md +336 -0
- package/agent/progress.template.yaml +161 -0
- package/agent/progress.yaml +145 -0
- package/agent/schemas/package.schema.yaml +276 -0
- package/agent/scripts/acp.common.sh +1781 -0
- package/agent/scripts/acp.install.sh +333 -0
- package/agent/scripts/acp.package-create.sh +924 -0
- package/agent/scripts/acp.package-info.sh +288 -0
- package/agent/scripts/acp.package-install.sh +893 -0
- package/agent/scripts/acp.package-list.sh +311 -0
- package/agent/scripts/acp.package-publish.sh +420 -0
- package/agent/scripts/acp.package-remove.sh +348 -0
- package/agent/scripts/acp.package-search.sh +156 -0
- package/agent/scripts/acp.package-update.sh +517 -0
- package/agent/scripts/acp.package-validate.sh +1018 -0
- package/agent/scripts/acp.uninstall.sh +85 -0
- package/agent/scripts/acp.version-check-for-updates.sh +98 -0
- package/agent/scripts/acp.version-check.sh +47 -0
- package/agent/scripts/acp.version-update.sh +176 -0
- package/agent/scripts/acp.yaml-parser.sh +985 -0
- package/agent/scripts/acp.yaml-validate.sh +205 -0
- package/agent/tasks/.gitkeep +0 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-1-initialize-tanstack-start-project.md +210 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-2-implement-data-model-yaml-parser.md +294 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-3-build-server-api-data-loading.md +193 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-4-add-auto-refresh-sse.md +262 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-10-polish-integration-testing.md +156 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-5-build-dashboard-layout-routing.md +178 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-6-build-overview-page.md +141 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-7-implement-milestone-table-view.md +153 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-8-implement-milestone-tree-view.md +174 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-9-implement-search-filtering.md +233 -0
- package/agent/tasks/task-1-{title}.template.md +244 -0
- package/bin/visualize.mjs +84 -0
- package/package.json +48 -0
- package/src/components/ExtraFieldsBadge.tsx +15 -0
- package/src/components/FilterBar.tsx +33 -0
- package/src/components/Header.tsx +23 -0
- package/src/components/MilestoneTable.tsx +167 -0
- package/src/components/MilestoneTree.tsx +84 -0
- package/src/components/ProgressBar.tsx +20 -0
- package/src/components/SearchInput.tsx +22 -0
- package/src/components/Sidebar.tsx +54 -0
- package/src/components/StatusBadge.tsx +23 -0
- package/src/components/StatusDot.tsx +12 -0
- package/src/components/TaskList.tsx +36 -0
- package/src/components/ViewToggle.tsx +31 -0
- package/src/lib/config.ts +8 -0
- package/src/lib/file-watcher.ts +43 -0
- package/src/lib/search.ts +48 -0
- package/src/lib/types.ts +73 -0
- package/src/lib/useAutoRefresh.ts +31 -0
- package/src/lib/useCollapse.ts +31 -0
- package/src/lib/useFilteredData.ts +55 -0
- package/src/lib/yaml-loader-real.spec.ts +47 -0
- package/src/lib/yaml-loader.spec.ts +201 -0
- package/src/lib/yaml-loader.ts +265 -0
- package/src/routeTree.gen.ts +140 -0
- package/src/router.tsx +10 -0
- package/src/routes/__root.tsx +75 -0
- package/src/routes/api/watch.ts +29 -0
- package/src/routes/index.tsx +115 -0
- package/src/routes/milestones.tsx +50 -0
- package/src/routes/search.tsx +84 -0
- package/src/routes/tasks.tsx +63 -0
- package/src/services/progress-database.service.ts +46 -0
- package/src/styles.css +25 -0
- package/tsconfig.json +24 -0
- package/vite.config.ts +16 -0
- package/vitest.config.ts +27 -0
|
@@ -0,0 +1,331 @@
|
|
|
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
|
|
@@ -0,0 +1,235 @@
|
|
|
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
|