🎉 init: impl dashboard overview
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import type { SystemStatus, RemoteLinkItem, SSEEventData } from '@/types/api'
|
||||
|
||||
interface UseKlalbSSEResult {
|
||||
status: SystemStatus | null
|
||||
links: RemoteLinkItem[]
|
||||
isConnected: boolean
|
||||
error: string | null
|
||||
reconnectAll: () => Promise<boolean>
|
||||
refreshStatus: () => Promise<void>
|
||||
}
|
||||
|
||||
export function useKlalbSSE(): UseKlalbSSEResult {
|
||||
const [status, setStatus] = useState<SystemStatus | null>(null)
|
||||
const [links, setLinks] = useState<RemoteLinkItem[]>([])
|
||||
const [isConnected, setIsConnected] = useState<boolean>(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const eventSourceRef = useRef<EventSource | null>(null)
|
||||
const reconnectTimeoutRef = useRef<number | null>(null)
|
||||
|
||||
const refreshStatus = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/status')
|
||||
if (res.ok) {
|
||||
const data = (await res.json()) as SystemStatus
|
||||
setStatus(data)
|
||||
setError(null)
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to fetch status fallback:', err)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const reconnectAll = useCallback(async (): Promise<boolean> => {
|
||||
try {
|
||||
const res = await fetch('/api/links/reconnect', { method: 'POST' })
|
||||
return res.ok
|
||||
} catch (err) {
|
||||
console.error('Failed to trigger reconnect all:', err)
|
||||
return false
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
let isUnmounted = false
|
||||
|
||||
function connectSSE() {
|
||||
if (isUnmounted) return
|
||||
|
||||
try {
|
||||
const es = new EventSource('/api/events')
|
||||
eventSourceRef.current = es
|
||||
|
||||
es.onopen = () => {
|
||||
if (!isUnmounted) {
|
||||
setIsConnected(true)
|
||||
setError(null)
|
||||
}
|
||||
}
|
||||
|
||||
es.onmessage = (event) => {
|
||||
if (isUnmounted) return
|
||||
try {
|
||||
const data = JSON.parse(event.data) as SSEEventData
|
||||
if (data.status) {
|
||||
setStatus(data.status)
|
||||
}
|
||||
if (data.links) {
|
||||
setLinks(data.links)
|
||||
}
|
||||
setIsConnected(true)
|
||||
setError(null)
|
||||
} catch (e) {
|
||||
console.error('Failed to parse SSE event data:', e)
|
||||
}
|
||||
}
|
||||
|
||||
es.onerror = () => {
|
||||
if (isUnmounted) return
|
||||
setIsConnected(false)
|
||||
setError('SSE connection lost, reconnecting...')
|
||||
es.close()
|
||||
eventSourceRef.current = null
|
||||
|
||||
// Fallback poll
|
||||
refreshStatus()
|
||||
|
||||
// Schedule reconnection in 2s
|
||||
if (reconnectTimeoutRef.current) {
|
||||
window.clearTimeout(reconnectTimeoutRef.current)
|
||||
}
|
||||
reconnectTimeoutRef.current = window.setTimeout(() => {
|
||||
connectSSE()
|
||||
}, 2000)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('SSE initialization error:', e)
|
||||
if (!isUnmounted) {
|
||||
setIsConnected(false)
|
||||
setError('Failed to establish event stream')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
refreshStatus()
|
||||
connectSSE()
|
||||
|
||||
return () => {
|
||||
isUnmounted = true
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close()
|
||||
eventSourceRef.current = null
|
||||
}
|
||||
if (reconnectTimeoutRef.current) {
|
||||
window.clearTimeout(reconnectTimeoutRef.current)
|
||||
}
|
||||
}
|
||||
}, [refreshStatus])
|
||||
|
||||
return {
|
||||
status,
|
||||
links,
|
||||
isConnected,
|
||||
error,
|
||||
reconnectAll,
|
||||
refreshStatus,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
|
||||
return !!isMobile
|
||||
}
|
||||
Reference in New Issue
Block a user