@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,369 +0,0 @@
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
@@ -1,145 +0,0 @@
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
@@ -1,93 +0,0 @@
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