cc-dashboard/web/src/composables/useSSE.ts
Vadym Samoilenko ff52d502b8 feat: add complete Vue 3 frontend in web/ directory
Full Vue 3 + Vite + TypeScript + Tailwind SPA replacing the vanilla JS static frontend.
Includes router, Pinia stores (auth/tasks/calendar/devops), axios API client with all
endpoints, UI components (Button/Card/Dialog/Badge/Input/etc), calendar grid with
lane-packing algorithm and DnD support, SSE live feed, and all 11 views.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 18:52:43 +01:00

105 lines
2.6 KiB
TypeScript

import { ref, onUnmounted } from 'vue'
export interface SSEEvent {
type: string
data: unknown
}
export function useSSE(url: string) {
const events = ref<SSEEvent[]>([])
const connected = ref(false)
const error = ref<string | null>(null)
let es: EventSource | null = null
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
let closed = false
function connect() {
if (closed) return
try {
es = new EventSource(url)
es.onopen = () => {
connected.value = true
error.value = null
}
es.onmessage = (e) => {
try {
const parsed = JSON.parse(e.data)
events.value.push({ type: 'message', data: parsed })
// Keep last 200 events
if (events.value.length > 200) events.value.shift()
} catch {
events.value.push({ type: 'message', data: e.data })
}
}
es.addEventListener('session_start', (e: MessageEvent) => {
try {
events.value.push({ type: 'session_start', data: JSON.parse(e.data) })
} catch {
events.value.push({ type: 'session_start', data: e.data })
}
if (events.value.length > 200) events.value.shift()
})
es.addEventListener('session_end', (e: MessageEvent) => {
try {
events.value.push({ type: 'session_end', data: JSON.parse(e.data) })
} catch {
events.value.push({ type: 'session_end', data: e.data })
}
if (events.value.length > 200) events.value.shift()
})
es.addEventListener('activity', (e: MessageEvent) => {
try {
events.value.push({ type: 'activity', data: JSON.parse(e.data) })
} catch {
events.value.push({ type: 'activity', data: e.data })
}
if (events.value.length > 200) events.value.shift()
})
es.onerror = () => {
connected.value = false
error.value = 'Connection lost, reconnecting...'
es?.close()
es = null
if (!closed) {
reconnectTimer = setTimeout(() => connect(), 5000)
}
}
} catch (err) {
error.value = 'Failed to connect to event stream'
if (!closed) {
reconnectTimer = setTimeout(() => connect(), 5000)
}
}
}
function disconnect() {
closed = true
if (reconnectTimer) clearTimeout(reconnectTimer)
es?.close()
es = null
connected.value = false
}
function clearEvents() {
events.value = []
}
onUnmounted(() => {
disconnect()
})
return {
events,
connected,
error,
connect,
disconnect,
clearEvents,
}
}