diff --git a/src/lib/sovd-api.ts b/src/lib/sovd-api.ts index 69fe9df..d7ae6cd 100644 --- a/src/lib/sovd-api.ts +++ b/src/lib/sovd-api.ts @@ -1738,26 +1738,44 @@ export class SovdApiClient { * @param onError Callback for errors * @returns Cleanup function to close the connection */ - subscribeFaultStream(onFault: (fault: Fault) => void, onError?: (error: Error) => void): () => void { + subscribeFaultStream( + onFaultConfirmed: (fault: Fault) => void, + onFaultCleared: (fault: Fault) => void, + onError?: (error: Error) => void + ): () => void { const eventSource = new EventSource(this.getUrl('faults/stream')); - eventSource.onmessage = (event) => { + const parseFault = (event: MessageEvent): Fault | null => { try { - // API may return raw fault format that needs transformation + // API returns: { event_type, fault, timestamp } const rawData = JSON.parse(event.data); - // Check if this is the raw API format (has fault_code) or already transformed - if ('fault_code' in rawData) { - const fault = this.transformFault(rawData); - onFault(fault); - } else { - // Already in Fault format - onFault(rawData as Fault); + const faultData = rawData.fault || rawData; + if ('fault_code' in faultData) { + return this.transformFault(faultData); } + return faultData as Fault; } catch { onError?.(new Error('Failed to parse fault event')); + return null; } }; + eventSource.addEventListener('fault_confirmed', (event: MessageEvent) => { + const fault = parseFault(event); + if (fault) onFaultConfirmed(fault); + }); + + eventSource.addEventListener('fault_cleared', (event: MessageEvent) => { + const fault = parseFault(event); + if (fault) onFaultCleared(fault); + }); + + // Fallback for unnamed events - treat as confirmed + eventSource.onmessage = (event: MessageEvent) => { + const fault = parseFault(event); + if (fault) onFaultConfirmed(fault); + }; + eventSource.onerror = () => { onError?.(new Error('Fault stream connection error')); }; diff --git a/src/lib/store.ts b/src/lib/store.ts index 64cb852..76c8c9a 100644 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -557,6 +557,10 @@ export const useAppStore = create()( // Load root entities after successful connection await get().loadRootEntities(); + + // Subscribe to fault stream for real-time toast notifications + get().subscribeFaultStream(); + return true; } catch (error) { const message = error instanceof Error ? error.message : 'Connection failed'; @@ -573,6 +577,9 @@ export const useAppStore = create()( // Stop execution polling get().stopExecutionPolling(); + // Unsubscribe from fault stream + get().unsubscribeFaultStream(); + set({ serverUrl: null, baseEndpoint: '', @@ -1392,6 +1399,7 @@ export const useAppStore = create()( } const cleanup = client.subscribeFaultStream( + // onFaultConfirmed (fault) => { const { faults } = get(); // Add or update fault in the list @@ -1407,6 +1415,15 @@ export const useAppStore = create()( } toast.warning(`Fault: ${fault.message}`, { autoClose: 5000 }); }, + // onFaultCleared - no toast here, clearFault() already shows one for UI-triggered clears + (fault) => { + const { faults } = get(); + const newFaults = faults.filter( + (f) => !(f.code === fault.code && f.entity_id === fault.entity_id) + ); + set({ faults: newFaults }); + }, + // onError (error) => { toast.error(`Fault stream error: ${error.message}`); }