File size: 2,031 Bytes
9dfccd9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68af3c5
 
 
9dfccd9
68af3c5
9dfccd9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import { useCallback, useEffect, useRef, useState } from 'react'
import { env } from '@/config/env'
import { useUIStore } from '@/stores/uiStore'

export interface AppNotification {
  id:        string
  type:      'query_answered' | 'escalation_spike' | 'breaking_change' | 'data_sync_failed' | 'knowledge_gap'
  message:   string
  timestamp: string
  read:      boolean
}

export function useNotifications() {
  const [notifications, setNotifications] = useState<AppNotification[]>([])
  const wsRef   = useRef<WebSocket | null>(null)
  const addToast = useUIStore.getState().addToast

  const connect = useCallback(() => {
    const ws = new WebSocket(`${env.wsBaseUrl}/ws`)
    wsRef.current = ws

    ws.onmessage = (evt) => {
      try {
        const msg = JSON.parse(evt.data as string)
        // Skip keepalive pings and any frame without a real message
        if (!msg.message || msg.type === 'ping') return
        const notification: AppNotification = {
          ...(msg as Omit<AppNotification, 'id' | 'read'>),
          id:   crypto.randomUUID(),
          read: false,
        }
        setNotifications((prev) => [notification, ...prev].slice(0, 50))
        addToast({ type: 'info', message: notification.message })
      } catch {
        // Non-JSON or unknown frame — ignore
      }
    }

    // Gracefully no-op if endpoint is 404 or unavailable — notifications are non-critical
    ws.onerror  = () => {}
    ws.onclose  = () => {}
  }, [addToast])

  const markRead = useCallback((id: string) => {
    setNotifications((prev) =>
      prev.map((n) => (n.id === id ? { ...n, read: true } : n)),
    )
  }, [])

  const markAllRead = useCallback(() => {
    setNotifications((prev) => prev.map((n) => ({ ...n, read: true })))
  }, [])

  const unreadCount = notifications.filter((n) => !n.read).length

  useEffect(() => {
    connect()
    return () => {
      wsRef.current?.close()
      wsRef.current = null
    }
  }, [connect])

  return { notifications, unreadCount, markRead, markAllRead }
}