@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.
Files changed (180) hide show
  1. package/README.md +68 -0
  2. package/agent/commands/acp.clarification-address.md +417 -0
  3. package/agent/commands/acp.clarification-capture.md +386 -0
  4. package/agent/commands/acp.clarification-create.md +437 -0
  5. package/agent/commands/acp.clarifications-research.md +326 -0
  6. package/agent/commands/acp.command-create.md +432 -0
  7. package/agent/commands/acp.design-create.md +286 -0
  8. package/agent/commands/acp.design-reference.md +355 -0
  9. package/agent/commands/acp.handoff.md +270 -0
  10. package/agent/commands/acp.index.md +423 -0
  11. package/agent/commands/acp.init.md +546 -0
  12. package/agent/commands/acp.package-create.md +895 -0
  13. package/agent/commands/acp.package-info.md +212 -0
  14. package/agent/commands/acp.package-install.md +539 -0
  15. package/agent/commands/acp.package-list.md +280 -0
  16. package/agent/commands/acp.package-publish.md +541 -0
  17. package/agent/commands/acp.package-remove.md +293 -0
  18. package/agent/commands/acp.package-search.md +307 -0
  19. package/agent/commands/acp.package-update.md +361 -0
  20. package/agent/commands/acp.package-validate.md +540 -0
  21. package/agent/commands/acp.pattern-create.md +386 -0
  22. package/agent/commands/acp.plan.md +587 -0
  23. package/agent/commands/acp.proceed.md +882 -0
  24. package/agent/commands/acp.project-create.md +675 -0
  25. package/agent/commands/acp.project-info.md +312 -0
  26. package/agent/commands/acp.project-list.md +226 -0
  27. package/agent/commands/acp.project-remove.md +379 -0
  28. package/agent/commands/acp.project-set.md +227 -0
  29. package/agent/commands/acp.project-update.md +307 -0
  30. package/agent/commands/acp.projects-restore.md +228 -0
  31. package/agent/commands/acp.projects-sync.md +347 -0
  32. package/agent/commands/acp.report.md +407 -0
  33. package/agent/commands/acp.resume.md +239 -0
  34. package/agent/commands/acp.sessions.md +301 -0
  35. package/agent/commands/acp.status.md +293 -0
  36. package/agent/commands/acp.sync.md +364 -0
  37. package/agent/commands/acp.task-create.md +500 -0
  38. package/agent/commands/acp.update.md +302 -0
  39. package/agent/commands/acp.validate.md +466 -0
  40. package/agent/commands/acp.version-check-for-updates.md +276 -0
  41. package/agent/commands/acp.version-check.md +191 -0
  42. package/agent/commands/acp.version-update.md +289 -0
  43. package/agent/commands/command.template.md +339 -0
  44. package/agent/commands/git.commit.md +526 -0
  45. package/agent/commands/git.init.md +514 -0
  46. package/agent/commands/tanstack-cloudflare.deploy.md +272 -0
  47. package/agent/commands/tanstack-cloudflare.tail.md +275 -0
  48. package/agent/design/.gitkeep +0 -0
  49. package/agent/design/design.template.md +154 -0
  50. package/agent/design/local.dashboard-layout-routing.md +288 -0
  51. package/agent/design/local.data-model-yaml-parsing.md +310 -0
  52. package/agent/design/local.search-filtering.md +331 -0
  53. package/agent/design/local.server-api-auto-refresh.md +235 -0
  54. package/agent/design/local.table-tree-views.md +299 -0
  55. package/agent/design/local.visualizer-requirements.md +349 -0
  56. package/agent/design/requirements.template.md +387 -0
  57. package/agent/index/.gitkeep +0 -0
  58. package/agent/index/acp.core.yaml +137 -0
  59. package/agent/index/local.main.template.yaml +37 -0
  60. package/agent/manifest.template.yaml +13 -0
  61. package/agent/manifest.yaml +302 -0
  62. package/agent/milestones/.gitkeep +0 -0
  63. package/agent/milestones/milestone-1-project-scaffold-data-pipeline.md +67 -0
  64. package/agent/milestones/milestone-1-{title}.template.md +206 -0
  65. package/agent/milestones/milestone-2-dashboard-views-interaction.md +79 -0
  66. package/agent/package.template.yaml +86 -0
  67. package/agent/patterns/.gitkeep +0 -0
  68. package/agent/patterns/bootstrap.template.md +1237 -0
  69. package/agent/patterns/pattern.template.md +382 -0
  70. package/agent/patterns/tanstack-cloudflare.acl-permissions.md +332 -0
  71. package/agent/patterns/tanstack-cloudflare.action-bar-item.md +416 -0
  72. package/agent/patterns/tanstack-cloudflare.api-route-handlers.md +401 -0
  73. package/agent/patterns/tanstack-cloudflare.auth-session-management.md +387 -0
  74. package/agent/patterns/tanstack-cloudflare.card-and-list.md +271 -0
  75. package/agent/patterns/tanstack-cloudflare.chat-engine.md +353 -0
  76. package/agent/patterns/tanstack-cloudflare.confirmation-tokens.md +346 -0
  77. package/agent/patterns/tanstack-cloudflare.durable-objects-websocket.md +516 -0
  78. package/agent/patterns/tanstack-cloudflare.email-service.md +431 -0
  79. package/agent/patterns/tanstack-cloudflare.expander.md +98 -0
  80. package/agent/patterns/tanstack-cloudflare.fcm-push.md +115 -0
  81. package/agent/patterns/tanstack-cloudflare.firebase-anonymous-sessions.md +441 -0
  82. package/agent/patterns/tanstack-cloudflare.firebase-auth.md +348 -0
  83. package/agent/patterns/tanstack-cloudflare.firebase-firestore.md +550 -0
  84. package/agent/patterns/tanstack-cloudflare.firebase-storage.md +369 -0
  85. package/agent/patterns/tanstack-cloudflare.form-controls.md +145 -0
  86. package/agent/patterns/tanstack-cloudflare.global-search-context.md +93 -0
  87. package/agent/patterns/tanstack-cloudflare.image-carousel.md +126 -0
  88. package/agent/patterns/tanstack-cloudflare.library-services.md +553 -0
  89. package/agent/patterns/tanstack-cloudflare.lightbox.md +169 -0
  90. package/agent/patterns/tanstack-cloudflare.markdown-content.md +115 -0
  91. package/agent/patterns/tanstack-cloudflare.mention-suggestions.md +98 -0
  92. package/agent/patterns/tanstack-cloudflare.modal.md +156 -0
  93. package/agent/patterns/tanstack-cloudflare.nextjs-to-tanstack-routing.md +461 -0
  94. package/agent/patterns/tanstack-cloudflare.notifications-engine.md +151 -0
  95. package/agent/patterns/tanstack-cloudflare.oauth-token-refresh.md +90 -0
  96. package/agent/patterns/tanstack-cloudflare.og-metadata.md +296 -0
  97. package/agent/patterns/tanstack-cloudflare.pagination.md +442 -0
  98. package/agent/patterns/tanstack-cloudflare.pill-input.md +220 -0
  99. package/agent/patterns/tanstack-cloudflare.provider-adapter.md +401 -0
  100. package/agent/patterns/tanstack-cloudflare.rate-limiting.md +323 -0
  101. package/agent/patterns/tanstack-cloudflare.scheduled-tasks.md +338 -0
  102. package/agent/patterns/tanstack-cloudflare.searchable-settings.md +375 -0
  103. package/agent/patterns/tanstack-cloudflare.slide-over.md +129 -0
  104. package/agent/patterns/tanstack-cloudflare.ssr-preload.md +571 -0
  105. package/agent/patterns/tanstack-cloudflare.third-party-api-integration.md +508 -0
  106. package/agent/patterns/tanstack-cloudflare.toast-system.md +142 -0
  107. package/agent/patterns/tanstack-cloudflare.unified-header.md +280 -0
  108. package/agent/patterns/tanstack-cloudflare.user-scoped-collections.md +628 -0
  109. package/agent/patterns/tanstack-cloudflare.websocket-manager.md +237 -0
  110. package/agent/patterns/tanstack-cloudflare.wrangler-configuration.md +358 -0
  111. package/agent/patterns/tanstack-cloudflare.zod-schema-validation.md +336 -0
  112. package/agent/progress.template.yaml +161 -0
  113. package/agent/progress.yaml +145 -0
  114. package/agent/schemas/package.schema.yaml +276 -0
  115. package/agent/scripts/acp.common.sh +1781 -0
  116. package/agent/scripts/acp.install.sh +333 -0
  117. package/agent/scripts/acp.package-create.sh +924 -0
  118. package/agent/scripts/acp.package-info.sh +288 -0
  119. package/agent/scripts/acp.package-install.sh +893 -0
  120. package/agent/scripts/acp.package-list.sh +311 -0
  121. package/agent/scripts/acp.package-publish.sh +420 -0
  122. package/agent/scripts/acp.package-remove.sh +348 -0
  123. package/agent/scripts/acp.package-search.sh +156 -0
  124. package/agent/scripts/acp.package-update.sh +517 -0
  125. package/agent/scripts/acp.package-validate.sh +1018 -0
  126. package/agent/scripts/acp.uninstall.sh +85 -0
  127. package/agent/scripts/acp.version-check-for-updates.sh +98 -0
  128. package/agent/scripts/acp.version-check.sh +47 -0
  129. package/agent/scripts/acp.version-update.sh +176 -0
  130. package/agent/scripts/acp.yaml-parser.sh +985 -0
  131. package/agent/scripts/acp.yaml-validate.sh +205 -0
  132. package/agent/tasks/.gitkeep +0 -0
  133. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-1-initialize-tanstack-start-project.md +210 -0
  134. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-2-implement-data-model-yaml-parser.md +294 -0
  135. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-3-build-server-api-data-loading.md +193 -0
  136. package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-4-add-auto-refresh-sse.md +262 -0
  137. package/agent/tasks/milestone-2-dashboard-views-interaction/task-10-polish-integration-testing.md +156 -0
  138. package/agent/tasks/milestone-2-dashboard-views-interaction/task-5-build-dashboard-layout-routing.md +178 -0
  139. package/agent/tasks/milestone-2-dashboard-views-interaction/task-6-build-overview-page.md +141 -0
  140. package/agent/tasks/milestone-2-dashboard-views-interaction/task-7-implement-milestone-table-view.md +153 -0
  141. package/agent/tasks/milestone-2-dashboard-views-interaction/task-8-implement-milestone-tree-view.md +174 -0
  142. package/agent/tasks/milestone-2-dashboard-views-interaction/task-9-implement-search-filtering.md +233 -0
  143. package/agent/tasks/task-1-{title}.template.md +244 -0
  144. package/bin/visualize.mjs +84 -0
  145. package/package.json +48 -0
  146. package/src/components/ExtraFieldsBadge.tsx +15 -0
  147. package/src/components/FilterBar.tsx +33 -0
  148. package/src/components/Header.tsx +23 -0
  149. package/src/components/MilestoneTable.tsx +167 -0
  150. package/src/components/MilestoneTree.tsx +84 -0
  151. package/src/components/ProgressBar.tsx +20 -0
  152. package/src/components/SearchInput.tsx +22 -0
  153. package/src/components/Sidebar.tsx +54 -0
  154. package/src/components/StatusBadge.tsx +23 -0
  155. package/src/components/StatusDot.tsx +12 -0
  156. package/src/components/TaskList.tsx +36 -0
  157. package/src/components/ViewToggle.tsx +31 -0
  158. package/src/lib/config.ts +8 -0
  159. package/src/lib/file-watcher.ts +43 -0
  160. package/src/lib/search.ts +48 -0
  161. package/src/lib/types.ts +73 -0
  162. package/src/lib/useAutoRefresh.ts +31 -0
  163. package/src/lib/useCollapse.ts +31 -0
  164. package/src/lib/useFilteredData.ts +55 -0
  165. package/src/lib/yaml-loader-real.spec.ts +47 -0
  166. package/src/lib/yaml-loader.spec.ts +201 -0
  167. package/src/lib/yaml-loader.ts +265 -0
  168. package/src/routeTree.gen.ts +140 -0
  169. package/src/router.tsx +10 -0
  170. package/src/routes/__root.tsx +75 -0
  171. package/src/routes/api/watch.ts +29 -0
  172. package/src/routes/index.tsx +115 -0
  173. package/src/routes/milestones.tsx +50 -0
  174. package/src/routes/search.tsx +84 -0
  175. package/src/routes/tasks.tsx +63 -0
  176. package/src/services/progress-database.service.ts +46 -0
  177. package/src/styles.css +25 -0
  178. package/tsconfig.json +24 -0
  179. package/vite.config.ts +16 -0
  180. package/vitest.config.ts +27 -0
@@ -0,0 +1,369 @@
1
+ # Firebase Storage
2
+
3
+ **Category**: Code
4
+ **Applicable To**: All file upload/download, signed URL generation, image proxy, content moderation, and storage cleanup
5
+ **Status**: Stable
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ This pattern covers how Firebase Storage is used via `@prmichaelsen/firebase-admin-sdk-v8` for file operations: chunked WebSocket uploads through a Durable Object, signed URL generation, server-side image proxy with ACL, content moderation via Google Cloud Vision, and storage cleanup on account deletion. The upload pipeline streams files from client → UploadManager DO → Firebase Storage with two-phase progress reporting.
12
+
13
+ ---
14
+
15
+ ## When to Use This Pattern
16
+
17
+ **Use this pattern when:**
18
+ - Adding a new file upload flow (new media type or storage path)
19
+ - Generating signed URLs for stored files
20
+ - Building an endpoint that serves or proxies stored files
21
+ - Adding content moderation to a new upload type
22
+ - Implementing storage cleanup for a new entity type
23
+
24
+ **Don't use this pattern when:**
25
+ - Storing structured data (use Firestore — see `tanstack-cloudflare.firebase-firestore`)
26
+ - Working with client-only file handling (canvas, blob URLs)
27
+ - Building external storage integrations (S3, R2, etc.)
28
+
29
+ ---
30
+
31
+ ## Core Principles
32
+
33
+ 1. **WebSocket Streaming**: Uploads use chunked WebSocket messages through a Durable Object, bypassing request size limits
34
+ 2. **Deny-All-Read Storage Rules**: Firebase Storage rules deny all direct reads — the image proxy (`/api/storage/image`) is the only access path
35
+ 3. **Fail-Open Moderation**: If Google Vision API is down, uploads proceed — best-effort safety, never block on transient failures
36
+ 4. **Metadata-Driven ACL**: File metadata (userId, conversationId) stored in Firebase is the source of truth for access control, not URL parameters
37
+ 5. **Two-Phase Progress**: 0–50% = client-to-DO transfer, 50–100% = DO-to-Firebase upload
38
+
39
+ ---
40
+
41
+ ## Implementation
42
+
43
+ ### Storage Path Structure
44
+
45
+ **File**: `src/constant/collections.ts`
46
+
47
+ ```typescript
48
+ export const STORAGE_BASE = BASE.replace(/\./g, '_')
49
+ // Paths: {STORAGE_BASE}/users/{userId}/{path}/{mediaId}
50
+ ```
51
+
52
+ | Media Type | Path | Example |
53
+ |---|---|---|
54
+ | Chat images | `['chat']` | `agentbase/users/abc/chat/media123` |
55
+ | Profile avatars | `['profile', 'avatar']` | `agentbase/users/abc/profile/avatar/media123` |
56
+ | Profile banners | `['profile', 'banner']` | `agentbase/users/abc/profile/banner/media123` |
57
+ | Widget images | `['profile', 'widgets']` | `agentbase/users/abc/profile/widgets/media123` |
58
+
59
+ ### Upload Flow
60
+
61
+ ```
62
+ Client (upload-client.ts) UploadManager DO Firebase Storage
63
+ │ │ │
64
+ ├─ WebSocket connect ──────────► │ │
65
+ ├─ init { userId, path, size } ──► │ │
66
+ │ ◄─── ready ────────────────── │ │
67
+ ├─ chunk (256KB base64) ────────► │ (buffers in memory) │
68
+ ├─ chunk ────────────────────────► │ │
69
+ ├─ chunk (final) ────────────────► │ │
70
+ │ (0-50% progress) │ │
71
+ │ ├─ Content moderation ────► │
72
+ │ ◄─── moderating ───────────── │ (Vision SafeSearch) │
73
+ │ ◄─── moderation_result ────── │ │
74
+ │ ├─ uploadFileResumable() ────► │
75
+ │ ◄─── firebase_progress ────── │ (1MB chunks, 50-100%) │
76
+ │ │ ◄───── upload complete ──── │
77
+ │ ├─ generateSignedUrl() ──────► │
78
+ │ ◄─── success { signedUrl } ─── │ ◄───── signed URL ──────── │
79
+ ```
80
+
81
+ ### Key SDK Functions
82
+
83
+ ```typescript
84
+ import {
85
+ uploadFileResumable, // Upload with progress tracking
86
+ generateSignedUrl, // Time-limited read URLs
87
+ downloadFile, // Download file buffer
88
+ getFileMetadata, // File metadata (content-type, custom metadata)
89
+ listFiles, // Paginated file listing
90
+ deleteFile, // Delete single file
91
+ } from '@prmichaelsen/firebase-admin-sdk-v8'
92
+ ```
93
+
94
+ ### UploadManager Durable Object
95
+
96
+ **File**: `src/durable-objects/UploadManager.ts`
97
+
98
+ Upload to Firebase with progress:
99
+
100
+ ```typescript
101
+ await uploadFileResumable(storagePath, completeBuffer, contentType, {
102
+ chunkSize: 1024 * 1024, // 1MB chunks to Firebase
103
+ onProgress: (uploaded, total) => {
104
+ const pct = 50 + Math.round((uploaded / total) * 50) // 50-100% phase
105
+ ws.send(JSON.stringify({ type: 'firebase_progress', progress: pct }))
106
+ },
107
+ metadata: {
108
+ userId,
109
+ path: path.join('/'),
110
+ uploadedAt: new Date().toISOString(),
111
+ ...customMetadata,
112
+ },
113
+ })
114
+ ```
115
+
116
+ Generate signed URL after upload:
117
+
118
+ ```typescript
119
+ const expiresAt = new Date(Date.now() + expiresIn * 1000)
120
+ const signedUrl = await generateSignedUrl(storagePath, {
121
+ action: 'read',
122
+ expires: expiresAt,
123
+ })
124
+ ```
125
+
126
+ ### Content Moderation
127
+
128
+ **File**: `src/constant/moderation.ts`
129
+
130
+ ```typescript
131
+ export const MODERATION_CONFIG = {
132
+ adult: {
133
+ reject: ['VERY_LIKELY'] as const,
134
+ warn: ['LIKELY', 'POSSIBLE'] as const,
135
+ },
136
+ violence: {
137
+ reject: ['VERY_LIKELY'] as const,
138
+ warn: ['LIKELY', 'POSSIBLE'] as const,
139
+ },
140
+ racy: {
141
+ reject: [] as const, // Never reject on racy alone
142
+ warn: [] as const,
143
+ },
144
+ failOpen: true, // Allow upload if Vision API fails
145
+ skipContentTypes: ['image/svg+xml'],
146
+ }
147
+ ```
148
+
149
+ Moderation is skipped for: video files, SVG images, and `skipModeration: true` uploads (e.g., profile avatars use a separate moderation flow).
150
+
151
+ ### Image Proxy (ACL Enforcement)
152
+
153
+ **File**: `src/routes/api/storage/image.tsx`
154
+
155
+ Access tiers (checked in order):
156
+ 1. **Owner**: Requesting user === file owner → allow
157
+ 2. **Profile images**: Path contains `/profile/` → allow (public)
158
+ 3. **Space content**: Path contains `/spaces/` → allow (public)
159
+ 4. **Chat DM**: Query conversation, validate participant membership
160
+ 5. **Chat Group**: Query conversation, validate `can_read` permission
161
+ 6. **Deny**: All others → 403
162
+
163
+ ```typescript
164
+ // Download and serve the file
165
+ const buffer = await downloadFile(storagePath)
166
+ const metadata = await getFileMetadata(storagePath)
167
+
168
+ return new Response(buffer, {
169
+ headers: {
170
+ 'Content-Type': metadata.contentType || 'application/octet-stream',
171
+ 'Cache-Control': isPublic ? 'public, max-age=3300' : 'private, max-age=3300',
172
+ 'X-Crop-X': cropData?.x?.toString() ?? '',
173
+ // ... other crop metadata headers
174
+ },
175
+ })
176
+ ```
177
+
178
+ ### MediaStorageService (Domain Wrapper)
179
+
180
+ **File**: `src/services/media-storage.service.ts`
181
+
182
+ High-level methods that set path and metadata for each use case:
183
+
184
+ ```typescript
185
+ static async saveChatImage(file, userId, conversationId, messageId, callbacks) {
186
+ return uploadToStorage(file, userId, {
187
+ path: ['chat'],
188
+ metadata: { conversationId, messageId, mediaType: 'image' },
189
+ ...callbacks,
190
+ })
191
+ }
192
+
193
+ static async saveProfileAvatar(file, userId, callbacks) {
194
+ return uploadToStorage(file, userId, {
195
+ path: ['profile', 'avatar'],
196
+ metadata: { mediaType: 'avatar' },
197
+ skipModeration: true, // Uses separate moderation flow
198
+ ...callbacks,
199
+ })
200
+ }
201
+ ```
202
+
203
+ ---
204
+
205
+ ## Examples
206
+
207
+ ### Example 1: Downloading a File for AI Processing
208
+
209
+ **File**: `src/lib/chat/message-formatter.ts`
210
+
211
+ ```typescript
212
+ import { downloadFile } from '@prmichaelsen/firebase-admin-sdk-v8'
213
+
214
+ const buffer = await downloadFile(storagePath)
215
+ let text = new TextDecoder('utf-8').decode(buffer)
216
+ const MAX_FILE_BYTES = 50 * 1024
217
+ if (text.length > MAX_FILE_BYTES) {
218
+ text = text.slice(0, MAX_FILE_BYTES) + '...[truncated]'
219
+ }
220
+ content.push({ type: 'text', text: `[File: ${fileName}]\n${text}\n[End File]` })
221
+ ```
222
+
223
+ ### Example 2: Bulk Storage Cleanup on Account Deletion
224
+
225
+ **File**: `src/services/account-deletion.service.ts`
226
+
227
+ ```typescript
228
+ import { listFiles, deleteFile } from '@prmichaelsen/firebase-admin-sdk-v8'
229
+
230
+ const prefix = `${STORAGE_BASE}/users/${userId}/`
231
+ let pageToken: string | undefined
232
+
233
+ do {
234
+ const listResult = await listFiles({
235
+ prefix,
236
+ maxResults: 500,
237
+ ...(pageToken ? { pageToken } : {}),
238
+ })
239
+
240
+ for (const file of listResult.files) {
241
+ await deleteFile(file.name)
242
+ }
243
+
244
+ pageToken = listResult.nextPageToken
245
+ } while (pageToken)
246
+ ```
247
+
248
+ ### Example 3: Crop Metadata Storage
249
+
250
+ **File**: `src/services/media-crop-database.service.ts`
251
+
252
+ Crop coordinates stored in Firestore at `{BASE}.users/{userId}/media-crops/{mediaId}`:
253
+
254
+ ```typescript
255
+ static async setCrop(userId: string, mediaId: string, storagePath: string, crop: CropData) {
256
+ const collection = getUserMediaCropsCollection(userId)
257
+ await setDocument(collection, mediaId, {
258
+ media_id: mediaId,
259
+ storage_path: storagePath,
260
+ crop,
261
+ created_at: new Date().toISOString(),
262
+ updated_at: new Date().toISOString(),
263
+ })
264
+ }
265
+ ```
266
+
267
+ Crop metadata returned as response headers from the image proxy:
268
+ `X-Crop-X`, `X-Crop-Y`, `X-Crop-Width`, `X-Crop-Height`, `X-Image-Width`, `X-Image-Height`
269
+
270
+ ---
271
+
272
+ ## Anti-Patterns
273
+
274
+ ### Serving Files Directly from Signed URLs
275
+
276
+ ```typescript
277
+ // Bad: Bypasses ACL — anyone with the URL can access
278
+ const url = await generateSignedUrl(path, { action: 'read', expires })
279
+ return new Response(JSON.stringify({ url })) // Client fetches directly
280
+
281
+ // Good: Proxy through server with ACL checks
282
+ const buffer = await downloadFile(path)
283
+ // ... validate ACL ...
284
+ return new Response(buffer, { headers: { 'Content-Type': contentType } })
285
+ ```
286
+
287
+ ### Trusting URL Parameters for ACL
288
+
289
+ ```typescript
290
+ // Bad: Client-supplied conversationId used for access check
291
+ const convId = url.searchParams.get('conversationId')
292
+
293
+ // Good: Read conversationId from file metadata (server source of truth)
294
+ const metadata = await getFileMetadata(storagePath)
295
+ const convId = metadata.metadata?.conversationId
296
+ ```
297
+
298
+ ### Blocking Uploads on Moderation Failure
299
+
300
+ ```typescript
301
+ // Bad: API error blocks the upload entirely
302
+ const result = await VisionService.analyzeImage(buffer)
303
+ if (!result) throw new Error('Moderation failed') // Blocks upload
304
+
305
+ // Good: Fail open — allow upload if Vision API is unavailable
306
+ try {
307
+ const result = await VisionService.analyzeImage(buffer)
308
+ if (shouldReject(result)) { reject(); return }
309
+ } catch {
310
+ // Vision API down — allow upload (fail-open)
311
+ }
312
+ ```
313
+
314
+ ---
315
+
316
+ ## Key Design Decisions
317
+
318
+ ### Upload Architecture
319
+
320
+ | Decision | Choice | Rationale |
321
+ |---|---|---|
322
+ | Upload transport | WebSocket via Durable Object | Bypasses HTTP body size limits; enables progress tracking |
323
+ | Chunk size (client→DO) | 256KB | Stays under 1MiB Cloudflare WebSocket message limit after base64 |
324
+ | Chunk size (DO→Firebase) | 1MB | Optimal for Firebase resumable uploads |
325
+ | DO instance key | `idFromName(userId)` | One DO per user; consistent instance across uploads |
326
+
327
+ ### Security
328
+
329
+ | Decision | Choice | Rationale |
330
+ |---|---|---|
331
+ | Storage rules | Deny-all-read | Forces all access through server-side ACL proxy |
332
+ | ACL source of truth | File metadata (not URL params) | Prevents parameter tampering |
333
+ | Moderation strategy | Fail-open, reject only VERY_LIKELY | Minimizes false positives while catching obvious violations |
334
+ | Video uploads | Blocked entirely | No moderation pipeline for video yet |
335
+
336
+ ### Performance
337
+
338
+ | Decision | Choice | Rationale |
339
+ |---|---|---|
340
+ | Image proxy caching | `max-age=3300` (~55 min) | Balances freshness with CDN efficiency |
341
+ | Usage tracking | Fire-and-forget after upload | Don't block upload success on quota tracking |
342
+ | Image compression | Client-side to ≤1568px, 0.85 JPEG | Claude-optimal size; reduces upload time |
343
+
344
+ ---
345
+
346
+ ## Checklist for Implementation
347
+
348
+ - [ ] Storage path uses `STORAGE_BASE` constant, not hardcoded prefix
349
+ - [ ] New upload type added to `MediaStorageService` with correct path and metadata
350
+ - [ ] Content moderation configured for new image upload types (skip for non-images)
351
+ - [ ] Image proxy updated with ACL rules for new access patterns
352
+ - [ ] `UsageDatabaseService.incrementStorage()` called after successful upload
353
+ - [ ] Account deletion cleanup handles new storage paths
354
+ - [ ] Client-side image compression applied before upload (if applicable)
355
+
356
+ ---
357
+
358
+ ## Related Patterns
359
+
360
+ - **[Firebase Auth](./tanstack-cloudflare.firebase-auth.md)**: Auth verification required before upload and download operations
361
+ - **[Firebase Firestore](./tanstack-cloudflare.firebase-firestore.md)**: Crop metadata and usage tracking stored in Firestore
362
+ - **[Database Service Conventions](./database-service-conventions.md)**: MediaCropDatabaseService follows standard conventions
363
+
364
+ ---
365
+
366
+ **Status**: Stable
367
+ **Recommendation**: Follow this pattern for all new file upload/download features. Always proxy through the image API — never expose signed URLs directly to clients.
368
+ **Last Updated**: 2026-03-14
369
+ **Contributors**: Community
@@ -0,0 +1,145 @@
1
+ # Form Controls: Slider & ToggleSwitch
2
+
3
+ **Category**: Design
4
+ **Applicable To**: Range inputs and boolean toggles
5
+ **Status**: Stable
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ Two reusable form control components: Slider (continuous/discrete range input with gradient fill) and ToggleSwitch (iOS-style boolean toggle with ARIA support). Both follow consistent dark theme styling and are fully keyboard accessible. For pagination controls (Paginator, PaginationToggle, InfiniteScrollSentinel, Virtuoso), see [Pagination Suite](./tanstack-cloudflare.pagination.md).
12
+
13
+ ---
14
+
15
+ ## Implementation
16
+
17
+ ### Slider
18
+
19
+ **File**: `src/components/Slider.tsx`
20
+
21
+ ```typescript
22
+ // Continuous mode
23
+ interface SliderContinuousProps {
24
+ min: number
25
+ max: number
26
+ step: number
27
+ value: number
28
+ onChange: (value: number) => void
29
+ }
30
+
31
+ // Discrete mode
32
+ interface SliderDiscreteProps {
33
+ options: Array<{ value: number; label?: string }>
34
+ value: number
35
+ onChange: (value: number) => void
36
+ }
37
+ ```
38
+
39
+ **Features**:
40
+ - **Continuous**: Standard min/max/step range input
41
+ - **Discrete**: Snaps to predefined option values, optional labels below
42
+ - **Gradient fill**: `linear-gradient(90deg, #3b82f6 0%, #8b5cf6 ${pct}%, rgb(55 65 81) ${pct}%)`
43
+ - Custom CSS class `slider-styled` in `styles.css`:
44
+ - Thumb: 20px circle, box-shadow, -7px margin-top
45
+ - Track: 6px height, 3px border-radius
46
+ - Keyboard: Left/Right arrows adjust value
47
+
48
+ **Usage**:
49
+ ```typescript
50
+ <Slider min={0} max={100} step={5} value={volume} onChange={setVolume} />
51
+
52
+ <Slider
53
+ options={[
54
+ { value: 0, label: 'Off' },
55
+ { value: 50, label: 'Medium' },
56
+ { value: 100, label: 'Max' },
57
+ ]}
58
+ value={level}
59
+ onChange={setLevel}
60
+ />
61
+ ```
62
+
63
+ ---
64
+
65
+ ### ToggleSwitch
66
+
67
+ **File**: `src/components/ToggleSwitch.tsx`
68
+
69
+ ```typescript
70
+ interface ToggleSwitchProps {
71
+ checked: boolean
72
+ onChange: (checked: boolean) => void
73
+ size?: 'sm' | 'md' | 'lg' // default: 'md'
74
+ label?: string
75
+ description?: string
76
+ disabled?: boolean
77
+ id?: string
78
+ }
79
+ ```
80
+
81
+ **Features**:
82
+ - iOS-style toggle with animated knob
83
+ - **Checked**: gradient background `from-purple-600 to-blue-600`, checkmark inside knob
84
+ - **Unchecked**: `bg-gray-700`, plain knob
85
+ - Size presets (md: track w-11 h-6, knob w-5 h-5)
86
+ - Keyboard: Space/Enter toggles
87
+ - `role="switch"`, `aria-checked` for accessibility
88
+ - `focus-visible:ring-2 ring-purple-500` focus ring
89
+ - Optional label + description text
90
+ - Disabled: `opacity-50 cursor-not-allowed`
91
+
92
+ **Usage**:
93
+ ```typescript
94
+ <ToggleSwitch
95
+ checked={darkMode}
96
+ onChange={setDarkMode}
97
+ label="Dark Mode"
98
+ description="Use dark color scheme throughout the app"
99
+ />
100
+ ```
101
+
102
+ ---
103
+
104
+ ## Anti-Patterns
105
+
106
+ ### Using Native Checkbox Instead of ToggleSwitch
107
+
108
+ ```typescript
109
+ // Bad: Inconsistent with app design
110
+ <input type="checkbox" checked={value} onChange={...} />
111
+
112
+ // Good: Use ToggleSwitch for visual consistency
113
+ <ToggleSwitch checked={value} onChange={setValue} label="Enable feature" />
114
+ ```
115
+
116
+ ### Inline Range Input Instead of Slider
117
+
118
+ ```typescript
119
+ // Bad: No gradient fill, no discrete mode support
120
+ <input type="range" min={0} max={100} />
121
+
122
+ // Good: Use Slider with gradient and optional discrete options
123
+ <Slider min={0} max={100} step={1} value={val} onChange={setVal} />
124
+ ```
125
+
126
+ ---
127
+
128
+ ## Checklist
129
+
130
+ - [ ] Use `Slider` for any range/value selection (not raw `<input type="range">`)
131
+ - [ ] Use `ToggleSwitch` for boolean settings (not checkboxes)
132
+ - [ ] ToggleSwitch has `role="switch"` and `aria-checked`
133
+ - [ ] All controls are keyboard accessible
134
+
135
+ ---
136
+
137
+ ## Related Patterns
138
+
139
+ - **[Pagination Suite](./tanstack-cloudflare.pagination.md)**: Paginator, PaginationToggle, InfiniteScrollSentinel, Virtuoso patterns
140
+
141
+ ---
142
+
143
+ **Status**: Stable
144
+ **Last Updated**: 2026-03-14
145
+ **Contributors**: Community
@@ -0,0 +1,93 @@
1
+ # Global Search Context
2
+
3
+ **Category**: Architecture
4
+ **Applicable To**: Cross-component state sharing without Redux — search queries, filters, or any key-scoped shared state
5
+ **Status**: Stable
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ A lightweight pub/sub mechanism using React Context + `useRef` that enables multiple components to share state by key without Redux. Components call `useGlobalSearch(key)` and get a `[value, setValue]` tuple. Setting a value notifies only subscribers of that key, avoiding unnecessary renders for unrelated keys.
12
+
13
+ ---
14
+
15
+ ## Implementation
16
+
17
+ **File**: `src/contexts/GlobalSearchContext.tsx`
18
+
19
+ ```typescript
20
+ interface Store {
21
+ values: Map<string, string>
22
+ subscribers: Map<string, Set<() => void>>
23
+ }
24
+
25
+ const GlobalSearchContext = createContext<Store>(/* ... */)
26
+
27
+ function GlobalSearchProvider({ children }: { children: ReactNode }) {
28
+ const storeRef = useRef<Store>({
29
+ values: new Map(),
30
+ subscribers: new Map(),
31
+ })
32
+ return (
33
+ <GlobalSearchContext.Provider value={storeRef.current}>
34
+ {children}
35
+ </GlobalSearchContext.Provider>
36
+ )
37
+ }
38
+
39
+ function useGlobalSearch(key: string): [string, (value: string) => void] {
40
+ const store = useContext(GlobalSearchContext)
41
+ const [, setTick] = useState(0) // Force re-render
42
+
43
+ useEffect(() => {
44
+ const bump = () => setTick(t => t + 1)
45
+ if (!store.subscribers.has(key)) store.subscribers.set(key, new Set())
46
+ store.subscribers.get(key)!.add(bump)
47
+ return () => { store.subscribers.get(key)?.delete(bump) }
48
+ }, [store, key])
49
+
50
+ const value = store.values.get(key) ?? ''
51
+
52
+ const setValue = useCallback((v: string) => {
53
+ const current = store.values.get(key) ?? ''
54
+ if (current === v) return // Skip if unchanged
55
+ if (v) store.values.set(key, v)
56
+ else store.values.delete(key)
57
+ store.subscribers.get(key)?.forEach(fn => fn())
58
+ }, [store, key])
59
+
60
+ return [value, setValue]
61
+ }
62
+ ```
63
+
64
+ **Usage**:
65
+
66
+ ```typescript
67
+ // Component A (search input)
68
+ const [query, setQuery] = useGlobalSearch('memories:query')
69
+ <input value={query} onChange={e => setQuery(e.target.value)} />
70
+
71
+ // Component B (feed list) — subscribes to same key
72
+ const [query] = useGlobalSearch('memories:query')
73
+ // Re-renders only when memories:query changes
74
+
75
+ // Component C (different key) — NOT re-rendered
76
+ const [filter] = useGlobalSearch('conversations:filter')
77
+ ```
78
+
79
+ **Key conventions**: `{page}:{field}` (e.g., `memories:query`, `conversations:filter`, `messages:query`)
80
+
81
+ ---
82
+
83
+ ## Checklist
84
+
85
+ - [ ] Wrap app in `<GlobalSearchProvider>` (in root layout)
86
+ - [ ] Use key convention `{page}:{field}` for scope isolation
87
+ - [ ] Don't use for non-search state — this is optimized for string values
88
+
89
+ ---
90
+
91
+ **Status**: Stable
92
+ **Last Updated**: 2026-03-14
93
+ **Contributors**: Community