* feat(gateway): support hot reload and empty startup - extract gateway runtime into pkg/gateway - add gateway.hot_reload config with default and example values - allow starting the gateway without a default model via --allow-empty - stop treating missing enabled channels as a startup error - update related tests * feat: replace gateway SSE updates with polling-based state sync - remove gateway SSE broadcasting and event endpoint - add polling-based gateway status refresh with stopping state handling - detect when gateway restart is required after default model changes - resolve gateway health and websocket proxy targets from configured host - update gateway UI labels and add backend/frontend test coverage
98 lines
2.5 KiB
TypeScript
98 lines
2.5 KiB
TypeScript
import { useAtomValue } from "jotai"
|
|
import { useEffect, useRef, useState } from "react"
|
|
|
|
import { clearGatewayLogs, getGatewayLogs } from "@/api/gateway"
|
|
import { gatewayAtom } from "@/store/gateway"
|
|
|
|
export function useGatewayLogs() {
|
|
const [logs, setLogs] = useState<string[]>([])
|
|
const [clearing, setClearing] = useState(false)
|
|
const logOffsetRef = useRef(0)
|
|
const logRunIdRef = useRef(-1)
|
|
const syncTokenRef = useRef(0)
|
|
|
|
const gateway = useAtomValue(gatewayAtom)
|
|
|
|
const clearLogs = async () => {
|
|
setClearing(true)
|
|
try {
|
|
const data = await clearGatewayLogs()
|
|
syncTokenRef.current += 1
|
|
setLogs([])
|
|
logOffsetRef.current = data.log_total ?? 0
|
|
if (data.log_run_id !== undefined) {
|
|
logRunIdRef.current = data.log_run_id
|
|
}
|
|
} catch {
|
|
// Ignore clear failures silently to avoid noisy transient errors.
|
|
} finally {
|
|
setClearing(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
let mounted = true
|
|
let timeout: ReturnType<typeof setTimeout>
|
|
|
|
const fetchLogs = async () => {
|
|
if (
|
|
!mounted ||
|
|
!["running", "starting", "restarting", "stopping"].includes(
|
|
gateway.status,
|
|
)
|
|
) {
|
|
if (mounted) {
|
|
timeout = setTimeout(fetchLogs, 1000)
|
|
}
|
|
return
|
|
}
|
|
|
|
try {
|
|
const requestToken = syncTokenRef.current
|
|
const requestOffset = logOffsetRef.current
|
|
const requestRunId = logRunIdRef.current
|
|
const data = await getGatewayLogs({
|
|
log_offset: requestOffset,
|
|
log_run_id: requestRunId,
|
|
})
|
|
|
|
if (!mounted || requestToken !== syncTokenRef.current) {
|
|
return
|
|
}
|
|
|
|
if (data.log_run_id !== undefined && data.log_run_id !== requestRunId) {
|
|
logRunIdRef.current = data.log_run_id
|
|
logOffsetRef.current = 0
|
|
if (data.logs) {
|
|
setLogs(data.logs)
|
|
logOffsetRef.current = data.log_total || data.logs.length
|
|
}
|
|
} else if (data.logs && data.logs.length > 0) {
|
|
const nextLogs = data.logs
|
|
setLogs((prev) => [...prev, ...nextLogs])
|
|
logOffsetRef.current =
|
|
data.log_total || logOffsetRef.current + nextLogs.length
|
|
}
|
|
} catch {
|
|
// Ignore simple fetch errors during polling.
|
|
} finally {
|
|
if (mounted) {
|
|
timeout = setTimeout(fetchLogs, 1000)
|
|
}
|
|
}
|
|
}
|
|
|
|
fetchLogs()
|
|
|
|
return () => {
|
|
mounted = false
|
|
clearTimeout(timeout)
|
|
}
|
|
}, [gateway.status])
|
|
|
|
return {
|
|
clearLogs,
|
|
clearing,
|
|
logs,
|
|
}
|
|
}
|