7.9 KiB
Enforcement WebSocket Streaming Implementation
Date: 2026-02-04
Feature: Real-time enforcement monitoring via WebSocket
Status: ✅ Implemented and Deployed
Overview
Implemented real-time WebSocket streaming for the enforcements page, matching the pattern used by the executions page. This allows users to see enforcement updates in real-time without manual page refreshes.
Problem
The enforcements page was using a simple invalidation approach for WebSocket notifications:
- When a notification arrived, it would invalidate the entire query cache and refetch
- This was inefficient and didn't support intelligent filtering
- The page lacked the sophisticated filtering and streaming capabilities present in the executions page
Solution
Created a dedicated useEnforcementStream hook and refactored the enforcements page to match the executions page pattern.
Changes Made
1. Created useEnforcementStream Hook
File: attune/web/src/hooks/useEnforcementStream.ts (new file)
Key features:
- Subscribes to enforcement entity notifications via WebSocket
- Updates React Query cache in real-time without full refetches
- Supports filtering by enforcement ID (for detail pages)
- Intelligently matches enforcement data against query parameters
- Updates existing enforcements in-place
- Adds new enforcements to lists when they match filters
- Invalidates related queries (rules and events)
Smart filtering logic:
function enforcementMatchesParams(enforcement: any, params: any): boolean {
// Check status filter
if (params.status && enforcement.status !== params.status) return false;
// Check event filter
if (params.event !== undefined && enforcement.event !== params.event) return false;
// Check rule filter
if (params.rule !== undefined && enforcement.rule !== params.rule) return false;
// Check trigger_ref filter
if (params.triggerRef && enforcement.trigger_ref !== params.triggerRef) return false;
return true;
}
2. Refactored Enforcements Page
File: attune/web/src/pages/enforcements/EnforcementsPage.tsx
Before:
- Simple filter inputs with immediate state updates
- Direct query invalidation on WebSocket notifications
- Limited filtering capabilities
After:
- Memoized filter components to prevent re-renders on WebSocket updates
- Debounced filter inputs (500ms for text, 300ms for selections)
- Multi-select status filter with client-side filtering for multiple selections
- Integrated
useEnforcementStreamhook - Real-time connection status indicator ("Live Updates")
- Consistent UI with executions page
Key improvements:
- Debouncing: Prevents excessive API calls while typing
- Memoization: Prevents unnecessary re-renders of filter inputs
- Multi-status filtering: API supports single status, client-side handles multiple
- Client-side rule_ref filtering: Supplements API filtering for unsupported fields
- Real-time updates: New enforcements appear instantly, existing ones update in-place
Architecture Pattern
The implementation follows the same pattern as executions:
WebSocket Notification → useEnforcementStream → React Query Cache Update → UI Re-render
↓
(No API refetch needed)
Benefits:
- Instant updates without full page refreshes
- Efficient cache management (update in-place, not refetch)
- Respects current filters and pagination
- Minimal network traffic
- Seamless user experience
User Experience
Real-time Features
- ✅ New enforcements appear at the top of the list instantly
- ✅ Status changes update in real-time
- ✅ Green "Live Updates" indicator shows connection status
- ✅ Filtering is responsive with debouncing
- ✅ Multiple status selections supported
Filter Options
- Rule (client-side): Filter by rule reference (e.g.,
core.on_timer) - Trigger (API): Filter by trigger reference (e.g.,
core.webhook) - Event ID (API): Filter by event ID (e.g.,
123) - Status (API + client-side): Single status via API, multiple via client
UI Consistency
- Matches executions page design and behavior
- Uses same filter components and styling
- Same "Live Updates" indicator
- Consistent table layout and navigation
Technical Details
WebSocket Integration
- Uses existing
useEntityNotificationscontext - Listens to "enforcement" entity type
- Processes notifications with stable callbacks
- Handles connection/disconnection gracefully
Query Cache Management
// Update specific enforcement query
queryClient.setQueryData(["enforcements", enforcementId], ...);
// Update enforcement lists
queryClient.getQueriesData({ queryKey: ["enforcements"], exact: false })
.forEach(([queryKey, oldData]) => {
// Smart merge logic here
});
// Invalidate related queries
queryClient.invalidateQueries({ queryKey: ["rules", ruleId, "enforcements"] });
queryClient.invalidateQueries({ queryKey: ["events", eventId, "enforcements"] });
Performance Optimizations
- Memoized components: Filter inputs don't re-render on WebSocket updates
- Debounced filters: Reduce API calls during typing
- Selective cache updates: Only update relevant queries
- Client-side filtering: Reduce server load for multi-status filters
Deployment
# Build web UI
cd web && npm run build
# Rebuild and restart web container
docker compose build web
docker compose restart web
Testing
Manual Testing Steps
- Open enforcements page
- Verify "Live Updates" indicator appears (green with pulse)
- Trigger a webhook or rule that creates an enforcement
- Verify new enforcement appears at the top instantly
- Apply various filters and verify they work correctly
- Select multiple statuses and verify client-side filtering works
- Check that existing enforcements update in real-time
Expected Behavior
- New enforcements appear without page refresh
- Status changes update immediately
- Filters are responsive with no lag
- Connection indicator shows accurate status
- Page remains usable during high-frequency updates
Comparison with Executions Page
| Feature | Executions Page | Enforcements Page |
|---|---|---|
| Real-time streaming | ✅ | ✅ |
| Debounced filters | ✅ | ✅ |
| Multi-select status | ✅ | ✅ |
| Memoized components | ✅ | ✅ |
| Live indicator | ✅ | ✅ |
| Smart cache updates | ✅ | ✅ |
| Client-side filtering | ✅ | ✅ |
Files Changed
- Created:
attune/web/src/hooks/useEnforcementStream.ts(185 lines) - Refactored:
attune/web/src/pages/enforcements/EnforcementsPage.tsx(456 → 384 lines, cleaner code)
Related Work
- Based on pattern from
useExecutionStreamhook - Uses existing
useEntityNotificationscontext - Leverages
MultiSelectcomponent from executions page - Consistent with overall WebSocket architecture
Future Improvements
Possible enhancements:
- Add pagination support with streaming (like executions)
- Add sorting options
- Add bulk actions (cancel multiple enforcements)
- Add export functionality
- Add advanced filtering (date ranges, payload inspection)
Impact
- ✅ Users can monitor enforcements in real-time
- ✅ Reduced server load (fewer polling requests)
- ✅ Improved user experience (instant updates)
- ✅ Consistent UI/UX across pages
- ✅ Better filtering capabilities
- ✅ More responsive interface
Lessons Learned
- Pattern reuse is valuable: The executions page pattern was well-designed and easily adaptable
- Debouncing is essential: Prevents filter spam during typing
- Memoization matters: Prevents unnecessary re-renders during streaming
- Client-side filtering complements API: Allows more flexible multi-filtering
- Consistent UX is important: Users benefit from familiar patterns across pages