@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,508 @@
1
+ # Third-Party API Integration Pattern
2
+
3
+ **Category**: Architecture
4
+ **Applicable To**: TanStack Start + Cloudflare Workers applications integrating with external APIs
5
+ **Status**: Stable
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ This pattern provides a structured approach for wrapping external APIs (property management systems, payment processors, search services, etc.) into modular, maintainable service modules. Each integration follows a consistent architecture: an auth/token module, an API client, domain-specific service methods, a type definitions file, and a barrel export.
12
+
13
+ The pattern ensures that external API complexity is contained within dedicated modules, with clean interfaces exposed to the rest of the application. It handles OAuth token lifecycle, sync operations, webhook processing, and error handling consistently across all integrations.
14
+
15
+ ---
16
+
17
+ ## When to Use This Pattern
18
+
19
+ ✅ **Use this pattern when:**
20
+ - Integrating with external REST APIs (Guesty, Stripe, Algolia, Mailchimp, etc.)
21
+ - External API requires OAuth token management or API key rotation
22
+ - Need to sync external data into your database
23
+ - Processing webhooks from external services
24
+ - Multiple parts of your app consume the same external API
25
+
26
+ ❌ **Don't use this pattern when:**
27
+ - Calling a single external endpoint once (inline fetch is fine)
28
+ - Using an SDK that already provides a clean interface
29
+ - The integration is purely client-side (e.g., Google Maps JS API)
30
+
31
+ ---
32
+
33
+ ## Core Principles
34
+
35
+ 1. **Modular File Structure**: Each integration gets its own directory with consistent file organization
36
+ 2. **Barrel Exports**: Single `index.ts` entry point per integration
37
+ 3. **Separated Concerns**: Auth, API client, domain services, types, and sync are separate files
38
+ 4. **Token Lifecycle Management**: Token storage, refresh, and expiration handled in dedicated module
39
+ 5. **Upsert Pattern for Sync**: External data synced via check-exists → update-or-create
40
+ 6. **Sync Logging**: All sync operations logged to a dedicated collection for debugging
41
+ 7. **Non-Blocking Errors**: Integration failures logged but don't crash the calling code
42
+
43
+ ---
44
+
45
+ ## Implementation
46
+
47
+ ### Structure
48
+
49
+ ```
50
+ src/lib/
51
+ └── {integration}/
52
+ ├── index.ts # Barrel exports
53
+ ├── types.ts # External API type definitions
54
+ ├── auth.ts # OAuth/token management
55
+ ├── api-client.ts # HTTP client wrapper
56
+ ├── token-storage.ts # Token persistence (Firestore/KV)
57
+ ├── {domain}.ts # Domain service (listings, reservations, etc.)
58
+ ├── sync.ts # Data sync logic
59
+ └── webhooks.ts # Webhook handler
60
+ ```
61
+
62
+ ### Code Example
63
+
64
+ #### Step 1: Define Types
65
+
66
+ ```typescript
67
+ // src/lib/guesty/types.ts
68
+
69
+ export interface GuestyListing {
70
+ _id: string
71
+ title: string
72
+ nickname: string
73
+ address: {
74
+ full: string
75
+ city: string
76
+ state: string
77
+ zipcode: string
78
+ lat: number
79
+ lng: number
80
+ }
81
+ bedrooms: number
82
+ bathrooms: number
83
+ accommodates: number
84
+ picture: { thumbnail: string; regular: string }
85
+ prices: { basePrice: number; currency: string }
86
+ active: boolean
87
+ }
88
+
89
+ export interface GuestyReservation {
90
+ _id: string
91
+ listingId: string
92
+ guestName: string
93
+ checkIn: string
94
+ checkOut: string
95
+ status: 'confirmed' | 'cancelled' | 'checked_in' | 'checked_out'
96
+ money: { totalPrice: number; currency: string }
97
+ }
98
+
99
+ export interface GuestyTokenResponse {
100
+ access_token: string
101
+ token_type: string
102
+ expires_in: number
103
+ }
104
+ ```
105
+
106
+ #### Step 2: Auth / Token Management
107
+
108
+ ```typescript
109
+ // src/lib/guesty/auth.ts
110
+
111
+ import { GuestyTokenStorage } from './token-storage'
112
+ import type { GuestyTokenResponse } from './types'
113
+
114
+ export class GuestyAuthService {
115
+ private static tokenEndpoint = 'https://open-api.guesty.com/oauth2/token'
116
+
117
+ /**
118
+ * Get a valid access token, refreshing if needed
119
+ */
120
+ static async getAccessToken(): Promise<string> {
121
+ // Check stored token
122
+ const stored = await GuestyTokenStorage.get()
123
+ if (stored && !this.isExpired(stored)) {
124
+ return stored.access_token
125
+ }
126
+
127
+ // Refresh token
128
+ return this.refreshToken()
129
+ }
130
+
131
+ /**
132
+ * Refresh the OAuth token
133
+ */
134
+ static async refreshToken(): Promise<string> {
135
+ const response = await fetch(this.tokenEndpoint, {
136
+ method: 'POST',
137
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
138
+ body: new URLSearchParams({
139
+ grant_type: 'client_credentials',
140
+ client_id: process.env.GUESTY_CLIENT_ID!,
141
+ client_secret: process.env.GUESTY_CLIENT_SECRET!,
142
+ }),
143
+ })
144
+
145
+ if (!response.ok) {
146
+ throw new Error(`Guesty token refresh failed: ${response.status}`)
147
+ }
148
+
149
+ const data: GuestyTokenResponse = await response.json()
150
+ await GuestyTokenStorage.store({
151
+ access_token: data.access_token,
152
+ expires_at: Date.now() + data.expires_in * 1000,
153
+ })
154
+
155
+ return data.access_token
156
+ }
157
+
158
+ private static isExpired(token: { expires_at: number }): boolean {
159
+ return Date.now() > token.expires_at - 60_000 // 1 minute buffer
160
+ }
161
+ }
162
+ ```
163
+
164
+ #### Step 3: API Client
165
+
166
+ ```typescript
167
+ // src/lib/guesty/api-client.ts
168
+
169
+ import { GuestyAuthService } from './auth'
170
+
171
+ export class GuestyApiClient {
172
+ private static baseUrl = 'https://open-api.guesty.com/v1'
173
+
174
+ /**
175
+ * Make an authenticated request to the Guesty API
176
+ */
177
+ static async request<T>(path: string, options: RequestInit = {}): Promise<T> {
178
+ const token = await GuestyAuthService.getAccessToken()
179
+
180
+ const response = await fetch(`${this.baseUrl}${path}`, {
181
+ ...options,
182
+ headers: {
183
+ 'Authorization': `Bearer ${token}`,
184
+ 'Content-Type': 'application/json',
185
+ ...options.headers,
186
+ },
187
+ })
188
+
189
+ if (!response.ok) {
190
+ const error = await response.text()
191
+ throw new Error(`Guesty API error ${response.status}: ${error}`)
192
+ }
193
+
194
+ return response.json()
195
+ }
196
+
197
+ static async get<T>(path: string): Promise<T> {
198
+ return this.request<T>(path)
199
+ }
200
+
201
+ static async post<T>(path: string, body: unknown): Promise<T> {
202
+ return this.request<T>(path, {
203
+ method: 'POST',
204
+ body: JSON.stringify(body),
205
+ })
206
+ }
207
+ }
208
+ ```
209
+
210
+ #### Step 4: Domain Service
211
+
212
+ ```typescript
213
+ // src/lib/guesty/listings.ts
214
+
215
+ import { GuestyApiClient } from './api-client'
216
+ import type { GuestyListing } from './types'
217
+
218
+ export class GuestyListingsService {
219
+ /**
220
+ * Fetch all active listings from Guesty
221
+ */
222
+ static async getListings(limit = 100): Promise<GuestyListing[]> {
223
+ const data = await GuestyApiClient.get<{ results: GuestyListing[] }>(
224
+ `/listings?limit=${limit}&fields=title,nickname,address,bedrooms,bathrooms,accommodates,picture,prices,active`
225
+ )
226
+ return data.results.filter(l => l.active)
227
+ }
228
+
229
+ /**
230
+ * Fetch a single listing by ID
231
+ */
232
+ static async getListing(id: string): Promise<GuestyListing> {
233
+ return GuestyApiClient.get<GuestyListing>(`/listings/${id}`)
234
+ }
235
+ }
236
+ ```
237
+
238
+ #### Step 5: Sync Service
239
+
240
+ ```typescript
241
+ // src/lib/guesty/sync.ts
242
+
243
+ import { GuestyListingsService } from './listings'
244
+ import { GuestyReservationsService } from './reservations'
245
+ import { PropertyDatabaseService } from '@/services/property-database.service'
246
+ import { ReservationDatabaseService } from '@/services/reservation-database.service'
247
+
248
+ interface SyncResult {
249
+ properties: { updated: number; created: number; errors: number }
250
+ reservations: { updated: number; created: number; errors: number }
251
+ syncedAt: string
252
+ }
253
+
254
+ export class GuestySyncService {
255
+ /**
256
+ * Full sync: pull all listings and reservations from Guesty
257
+ */
258
+ static async fullSync(): Promise<SyncResult> {
259
+ console.log('[GuestySync] Starting full sync...')
260
+
261
+ // Sync properties
262
+ const listings = await GuestyListingsService.getListings()
263
+ const propResult = await this.syncProperties(listings)
264
+
265
+ // Sync reservations
266
+ const reservations = await GuestyReservationsService.getReservations()
267
+ const resResult = await this.syncReservations(reservations)
268
+
269
+ const result: SyncResult = {
270
+ properties: propResult,
271
+ reservations: resResult,
272
+ syncedAt: new Date().toISOString(),
273
+ }
274
+
275
+ // Log sync results
276
+ await this.logSync(result)
277
+ console.log('[GuestySync] Complete:', result)
278
+
279
+ return result
280
+ }
281
+
282
+ /**
283
+ * Upsert pattern: check if exists, update or create
284
+ */
285
+ private static async syncProperties(listings: GuestyListing[]) {
286
+ let updated = 0, created = 0, errors = 0
287
+
288
+ for (const listing of listings) {
289
+ try {
290
+ const existing = await PropertyDatabaseService.getByExternalId(listing._id)
291
+ const mapped = this.mapListingToProperty(listing)
292
+
293
+ if (existing) {
294
+ await PropertyDatabaseService.update(existing.id, mapped)
295
+ updated++
296
+ } else {
297
+ await PropertyDatabaseService.create(mapped)
298
+ created++
299
+ }
300
+ } catch (error) {
301
+ console.error(`[GuestySync] Failed to sync listing ${listing._id}:`, error)
302
+ errors++
303
+ }
304
+ }
305
+
306
+ return { updated, created, errors }
307
+ }
308
+
309
+ private static mapListingToProperty(listing: GuestyListing) {
310
+ return {
311
+ external_id: listing._id,
312
+ external_source: 'guesty',
313
+ title: listing.title,
314
+ address: listing.address.full,
315
+ bedrooms: listing.bedrooms,
316
+ bathrooms: listing.bathrooms,
317
+ capacity: listing.accommodates,
318
+ image_url: listing.picture?.regular,
319
+ base_price: listing.prices?.basePrice,
320
+ currency: listing.prices?.currency,
321
+ updated_at: new Date().toISOString(),
322
+ }
323
+ }
324
+
325
+ private static async logSync(result: SyncResult): Promise<void> {
326
+ // Store sync log for debugging and audit trail
327
+ await SyncLogDatabaseService.create({
328
+ source: 'guesty',
329
+ result,
330
+ created_at: new Date().toISOString(),
331
+ })
332
+ }
333
+ }
334
+ ```
335
+
336
+ #### Step 6: Barrel Export
337
+
338
+ ```typescript
339
+ // src/lib/guesty/index.ts
340
+
341
+ export { GuestyAuthService } from './auth'
342
+ export { GuestyApiClient } from './api-client'
343
+ export { GuestyListingsService } from './listings'
344
+ export { GuestyReservationsService } from './reservations'
345
+ export { GuestySyncService } from './sync'
346
+ export { GuestyWebhooksService } from './webhooks'
347
+ export { GuestyTokenStorage } from './token-storage'
348
+ export type * from './types'
349
+ ```
350
+
351
+ #### Step 7: Webhook Handler
352
+
353
+ ```typescript
354
+ // routes/api/webhooks/guesty.tsx
355
+ import { createFileRoute } from '@tanstack/react-router'
356
+ import { GuestyWebhooksService } from '@/lib/guesty'
357
+
358
+ export const Route = createFileRoute('/api/webhooks/guesty')({
359
+ server: {
360
+ handlers: {
361
+ POST: async ({ request }) => {
362
+ try {
363
+ const body = await request.json()
364
+ const signature = request.headers.get('x-guesty-signature')
365
+
366
+ // Verify webhook signature
367
+ if (!GuestyWebhooksService.verifySignature(body, signature)) {
368
+ return new Response(JSON.stringify({ error: 'Invalid signature' }), {
369
+ status: 401,
370
+ headers: { 'Content-Type': 'application/json' },
371
+ })
372
+ }
373
+
374
+ // Process webhook
375
+ await GuestyWebhooksService.handleWebhook(body)
376
+
377
+ return new Response(JSON.stringify({ received: true }), {
378
+ status: 200,
379
+ headers: { 'Content-Type': 'application/json' },
380
+ })
381
+ } catch (error) {
382
+ console.error('[Webhook] Guesty webhook error:', error)
383
+ // Return 200 to prevent webhook retries for processing errors
384
+ return new Response(JSON.stringify({ received: true, error: 'Processing failed' }), {
385
+ status: 200,
386
+ headers: { 'Content-Type': 'application/json' },
387
+ })
388
+ }
389
+ },
390
+ },
391
+ },
392
+ })
393
+ ```
394
+
395
+ ---
396
+
397
+ ## Integration Checklist Template
398
+
399
+ For each new integration, create:
400
+
401
+ | File | Purpose |
402
+ |------|---------|
403
+ | `types.ts` | External API response types |
404
+ | `auth.ts` | Token management (OAuth, API keys) |
405
+ | `api-client.ts` | Authenticated HTTP client |
406
+ | `token-storage.ts` | Token persistence |
407
+ | `{domain}.ts` | Domain-specific operations |
408
+ | `sync.ts` | Data synchronization (if needed) |
409
+ | `webhooks.ts` | Webhook processing (if needed) |
410
+ | `index.ts` | Barrel exports |
411
+
412
+ ---
413
+
414
+ ## Benefits
415
+
416
+ ### 1. Contained Complexity
417
+ All integration logic lives in one directory — easy to find, modify, or remove.
418
+
419
+ ### 2. Consistent Pattern
420
+ Every integration follows the same structure — onboarding new integrations is predictable.
421
+
422
+ ### 3. Token Lifecycle Managed
423
+ Token refresh, expiration, and storage are handled transparently by the auth module.
424
+
425
+ ### 4. Testable
426
+ Each module can be unit tested independently. API client can be mocked for service tests.
427
+
428
+ ---
429
+
430
+ ## Trade-offs
431
+
432
+ ### 1. Boilerplate per Integration
433
+ **Downside**: Each integration requires 5-8 files.
434
+ **Mitigation**: The structure is consistent and can be scaffolded. Complexity is proportional to the API's complexity.
435
+
436
+ ### 2. No SDK Reuse
437
+ **Downside**: Custom API clients instead of official SDKs.
438
+ **Mitigation**: Some SDKs don't work in Workers (Node.js-specific). Custom clients give full control over auth and error handling.
439
+
440
+ ---
441
+
442
+ ## Anti-Patterns
443
+
444
+ ### ❌ Anti-Pattern: Inline API Calls
445
+
446
+ ```typescript
447
+ // ❌ BAD: Guesty calls scattered across codebase
448
+ const token = await fetch('https://open-api.guesty.com/oauth2/token', { ... })
449
+ const listings = await fetch('https://open-api.guesty.com/v1/listings', {
450
+ headers: { Authorization: `Bearer ${token}` }
451
+ })
452
+
453
+ // ✅ GOOD: Use integration module
454
+ import { GuestyListingsService } from '@/lib/guesty'
455
+ const listings = await GuestyListingsService.getListings()
456
+ ```
457
+
458
+ ### ❌ Anti-Pattern: Swallowing Webhook Errors Silently
459
+
460
+ ```typescript
461
+ // ❌ BAD: No logging on webhook failure
462
+ POST: async ({ request }) => {
463
+ try { await processWebhook(body) } catch {}
464
+ return Response.json({ received: true })
465
+ }
466
+
467
+ // ✅ GOOD: Log errors, still return 200
468
+ POST: async ({ request }) => {
469
+ try { await processWebhook(body) } catch (error) {
470
+ console.error('[Webhook] Processing failed:', error)
471
+ }
472
+ return Response.json({ received: true })
473
+ }
474
+ ```
475
+
476
+ ---
477
+
478
+ ## Related Patterns
479
+
480
+ - **[Library Services Pattern](./tanstack-cloudflare.library-services.md)**: Database services consume synced data
481
+ - **[Scheduled Tasks](./tanstack-cloudflare.scheduled-tasks.md)**: Token refresh and sync as cron jobs
482
+ - **[API Route Handlers](./tanstack-cloudflare.api-route-handlers.md)**: Webhook endpoints as API routes
483
+ - **[Zod Schema Validation](./tanstack-cloudflare.zod-schema-validation.md)**: Validate external API responses
484
+
485
+ ---
486
+
487
+ ## Checklist for Implementation
488
+
489
+ - [ ] Dedicated directory per integration (`src/lib/{integration}/`)
490
+ - [ ] `types.ts` with external API response types
491
+ - [ ] `auth.ts` with token management
492
+ - [ ] `api-client.ts` with authenticated HTTP wrapper
493
+ - [ ] `token-storage.ts` for token persistence
494
+ - [ ] Domain service files for specific operations
495
+ - [ ] `sync.ts` for data synchronization (if applicable)
496
+ - [ ] `webhooks.ts` for webhook handling (if applicable)
497
+ - [ ] `index.ts` barrel export
498
+ - [ ] Sync operations use upsert pattern
499
+ - [ ] Sync results logged to dedicated collection
500
+ - [ ] Webhook endpoints return 200 even on processing errors
501
+ - [ ] Token refresh handles expiration with buffer time
502
+
503
+ ---
504
+
505
+ **Status**: Stable - Proven pattern for external API integrations
506
+ **Recommendation**: Use for all external API integrations beyond trivial single-endpoint calls
507
+ **Last Updated**: 2026-02-28
508
+ **Contributors**: Patrick Michaelsen
@@ -0,0 +1,142 @@
1
+ # Toast System
2
+
3
+ **Category**: Design
4
+ **Applicable To**: All success/error/warning/info feedback for user actions
5
+ **Status**: Stable
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ The toast system wraps `@prmichaelsen/pretty-toasts` with two hooks: `useToast` for direct toast calls and `useActionToast` for wrapping async operations with automatic success/error feedback. Toasts render at z-60 (above modals) via a `StandaloneToastContainer` in the root layout.
12
+
13
+ ---
14
+
15
+ ## Implementation
16
+
17
+ ### useToast (Direct Toasts)
18
+
19
+ **File**: `src/hooks/useToast.ts`
20
+
21
+ ```typescript
22
+ const toast = useToast()
23
+
24
+ toast.success({ title: 'Saved!', message: 'Your changes have been saved.' })
25
+ toast.error({ title: 'Failed', message: 'Could not save changes.' })
26
+ toast.warning({ title: 'Warning', message: 'This action is irreversible.' })
27
+ toast.info({ title: 'Info', message: 'New version available.' })
28
+ ```
29
+
30
+ **Toast Options**:
31
+
32
+ ```typescript
33
+ interface ToastOptions {
34
+ title: string
35
+ message?: string
36
+ duration?: number // default: 2500ms
37
+ }
38
+ ```
39
+
40
+ ### useActionToast (Async Action Wrapper)
41
+
42
+ **File**: `src/hooks/useActionToast.ts`
43
+
44
+ ```typescript
45
+ const { withToast } = useActionToast()
46
+
47
+ const result = await withToast(
48
+ async () => {
49
+ await SomeService.doThing()
50
+ return { id: '123' }
51
+ },
52
+ {
53
+ success: { title: 'Done!', message: 'Thing completed.' },
54
+ error: { title: 'Failed', message: 'Could not do thing.' },
55
+ }
56
+ )
57
+ // result is the action's return value on success, undefined on error
58
+ ```
59
+
60
+ **Behavior**:
61
+ - Calls the async action
62
+ - On success: shows success toast, returns action result
63
+ - On error: shows error toast, returns `undefined`
64
+ - No try/catch needed at call site
65
+
66
+ ### Root Layout Integration
67
+
68
+ **File**: `src/routes/__root.tsx`
69
+
70
+ ```typescript
71
+ <ToastProvider>
72
+ <AuthProvider>
73
+ {/* ... all other providers ... */}
74
+ {children}
75
+ </AuthProvider>
76
+ <StandaloneToastContainer /> {/* z-60, above modals */}
77
+ </ToastProvider>
78
+ ```
79
+
80
+ ---
81
+
82
+ ## Examples
83
+
84
+ ### Inline Action with Toast
85
+
86
+ ```typescript
87
+ const handleDelete = async () => {
88
+ await withToast(
89
+ () => MemoryService.deleteMemory(memoryId),
90
+ {
91
+ success: { title: 'Deleted', message: 'Memory moved to trash.' },
92
+ error: { title: 'Delete failed' },
93
+ }
94
+ )
95
+ }
96
+ ```
97
+
98
+ ### Direct Toast for Non-Async Feedback
99
+
100
+ ```typescript
101
+ const handleCopy = () => {
102
+ navigator.clipboard.writeText(url)
103
+ toast.success({ title: 'Copied!', message: 'Link copied to clipboard.' })
104
+ }
105
+ ```
106
+
107
+ ---
108
+
109
+ ## Anti-Patterns
110
+
111
+ ### Manual Try/Catch + Toast
112
+
113
+ ```typescript
114
+ // Bad: Verbose boilerplate
115
+ try {
116
+ await SomeService.doThing()
117
+ toast.success({ title: 'Done!' })
118
+ } catch (err) {
119
+ toast.error({ title: 'Failed', message: err.message })
120
+ }
121
+
122
+ // Good: Use withToast
123
+ await withToast(() => SomeService.doThing(), {
124
+ success: { title: 'Done!' },
125
+ error: { title: 'Failed' },
126
+ })
127
+ ```
128
+
129
+ ---
130
+
131
+ ## Checklist
132
+
133
+ - [ ] Use `withToast` for all async user actions (save, delete, publish, etc.)
134
+ - [ ] Use `toast.success/error` directly for synchronous feedback (copy, toggle)
135
+ - [ ] Toast container is rendered once in root layout (not per-component)
136
+ - [ ] Keep toast messages concise — title is required, message is optional
137
+
138
+ ---
139
+
140
+ **Status**: Stable
141
+ **Last Updated**: 2026-03-14
142
+ **Contributors**: Community