@@ -271,6 +271,172 @@ import Auth from "./pages/auth/Auth";
271271
272272---
273273
274+ ## Error Handling with AppError
275+
276+ Use the centralized error utilities for consistent error handling:
277+
278+ ``` tsx
279+ import { AppError , ErrorCode , getErrorMessage , fromSupabaseError } from ' @/lib/errors' ;
280+ import { logger } from ' @/lib/logger' ;
281+
282+ // Convert Supabase errors to AppError
283+ const { data, error } = await supabase .from (' jobs' ).select (' *' );
284+ if (error ) {
285+ throw fromSupabaseError (error , { operation: ' fetchJobs' });
286+ }
287+
288+ // Safe error message extraction
289+ try {
290+ await someOperation ();
291+ } catch (error ) {
292+ const message = getErrorMessage (error ); // Always returns string
293+ logger .error (' Operation failed' , error , { operation: ' myOperation' });
294+ toast .error (message );
295+ }
296+
297+ // User-friendly error messages
298+ const appError = fromSupabaseError (error );
299+ toast .error (appError .toUserMessage ()); // "The requested resource was not found."
300+ ```
301+
302+ For detailed error handling patterns, see [ docs/ERROR_HANDLING.md] ( ./ERROR_HANDLING.md ) .
303+
304+ ---
305+
306+ ## Structured Logging
307+
308+ Use the logger for consistent, context-aware logging:
309+
310+ ``` tsx
311+ import { logger , createScopedLogger } from ' @/lib/logger' ;
312+
313+ // Basic logging with context
314+ logger .info (' Job created' , {
315+ operation: ' createJob' ,
316+ entityType: ' job' ,
317+ entityId: jobId ,
318+ tenantId ,
319+ });
320+
321+ logger .error (' Failed to update operation' , error , {
322+ operation: ' updateOperation' ,
323+ entityId: operationId ,
324+ });
325+
326+ // Scoped logger for hooks/components
327+ const log = createScopedLogger ({
328+ operation: ' useJobOperations' ,
329+ entityType: ' job' ,
330+ });
331+
332+ log .info (' Fetching job data' );
333+ log .error (' Failed to fetch' , error );
334+
335+ // Performance timing
336+ const data = await logger .timedAsync (' fetchMetrics' , async () => {
337+ return await fetchAllMetrics ();
338+ });
339+ ```
340+
341+ ---
342+
343+ ## Realtime Subscriptions with Filtering
344+
345+ ** Always filter subscriptions to reduce scope and prevent cascade refetches:**
346+
347+ ``` tsx
348+ import { useDebouncedCallback } from ' @/hooks/useDebounce' ;
349+
350+ // GOOD - filtered by tenant_id with debouncing
351+ useEffect (() => {
352+ if (! tenantId ) return ;
353+
354+ const debouncedFetch = useDebouncedCallback (fetchData , 200 );
355+
356+ const channel = supabase
357+ .channel (` operations-${tenantId } ` )
358+ .on (' postgres_changes' , {
359+ event: ' *' ,
360+ schema: ' public' ,
361+ table: ' operations' ,
362+ filter: ` tenant_id=eq.${tenantId } ` , // Filter to reduce scope
363+ }, () => {
364+ debouncedFetch (); // Debounce to prevent cascade
365+ })
366+ .subscribe ();
367+
368+ return () => channel .unsubscribe ();
369+ }, [tenantId ]);
370+
371+ // BAD - no filter, triggers on ALL operations across all tenants
372+ .on (' postgres_changes' , {
373+ event: ' *' ,
374+ schema: ' public' ,
375+ table: ' operations' ,
376+ // No filter - will trigger on every operation change!
377+ }, () => fetchData ())
378+ ```
379+
380+ ** Or use the reusable subscription hook:**
381+
382+ ``` tsx
383+ import { useRealtimeSubscription , useTenantSubscription } from ' @/hooks/useRealtimeSubscription' ;
384+
385+ // Tenant-scoped subscription
386+ useTenantSubscription (
387+ ' operations' ,
388+ tenantId ,
389+ () => refetch (),
390+ { debounceMs: 200 }
391+ );
392+
393+ // Multiple tables
394+ useRealtimeSubscription ({
395+ channelName: ' production-updates' ,
396+ tables: [
397+ { table: ' operations' , filter: ` cell_id=eq.${cellId } ` },
398+ { table: ' time_entries' , filter: ` tenant_id=eq.${tenantId } ` },
399+ ],
400+ onDataChange: handleUpdate ,
401+ debounceMs: 300 ,
402+ });
403+ ```
404+
405+ ---
406+
407+ ## QueryKeys and Cache Management
408+
409+ Use the QueryKeys factory for consistent cache keys:
410+
411+ ``` tsx
412+ import { QueryKeys , StaleTime , CacheTime } from ' @/lib/queryClient' ;
413+ import { invalidateOperationCaches } from ' @/lib/cacheInvalidation' ;
414+
415+ // Query with standard key
416+ const { data } = useQuery ({
417+ queryKey: QueryKeys .jobs .all (tenantId ),
418+ queryFn: fetchJobs ,
419+ staleTime: StaleTime .SHORT , // 30 seconds
420+ });
421+
422+ // Invalidate related caches on mutation
423+ const mutation = useMutation ({
424+ mutationFn: updateOperation ,
425+ onSuccess : () => {
426+ invalidateOperationCaches (queryClient , tenantId , operationId , cellId );
427+ },
428+ });
429+ ```
430+
431+ ** Stale time presets:**
432+ - ` StaleTime.VERY_SHORT ` - 10s (active operations, work queue)
433+ - ` StaleTime.SHORT ` - 30s (job lists, default)
434+ - ` StaleTime.MEDIUM ` - 2min (cell configurations)
435+ - ` StaleTime.LONG ` - 5min (user profiles)
436+ - ` StaleTime.VERY_LONG ` - 15min (app settings)
437+
438+ ---
439+
274440## Quick Reference Commands
275441
276442``` bash
0 commit comments