import { useCallback, useEffect, useRef, useState } from "react"; import { ApiError, api, openEventSocket, type ConnectRequest, type ConsoleState, type EventSocketStatus, type LiveRequest, type ReplayRequest, } from "./api"; export type BackendStatus = "checking" | "online" | "degraded" | "offline"; export type PendingAction = "scan" | "connect" | "live" | "replay" | "stop"; function messageFor(error: unknown): string { if (error instanceof ApiError) { return error.status ? `${error.message} (HTTP ${error.status})` : error.message; } return "Запрос к локальному API K1 завершился ошибкой."; } function measuredLatency(state: ConsoleState | null): number | null { const metrics = state?.metrics; if (!metrics) return null; const reported = metrics.pipeline_ms ?? metrics.end_to_end_ms; if (typeof reported === "number" && Number.isFinite(reported)) return reported; const segments = [ metrics.mqtt_to_decode_ms, metrics.decode_ms, metrics.publish_ms, ].filter((value): value is number => typeof value === "number" && Number.isFinite(value)); return segments.length ? segments.reduce((total, value) => total + value, 0) : null; } export function useK1Console() { const [state, setState] = useState(null); const [backendStatus, setBackendStatus] = useState("checking"); const [eventStatus, setEventStatus] = useState("connecting"); const [pendingAction, setPendingAction] = useState(null); const [error, setError] = useState(null); const [latencyHistory, setLatencyHistory] = useState([]); const mounted = useRef(true); const acceptState = useCallback((nextState: ConsoleState) => { setState(nextState); setBackendStatus("online"); }, []); const refresh = useCallback(async (reportErrors = true) => { const [healthResult, stateResult] = await Promise.allSettled([ api.getHealth(), api.getState(), ]); if (!mounted.current) return; if (stateResult.status === "fulfilled") { acceptState(stateResult.value); if (reportErrors) setError(null); } if (healthResult.status === "fulfilled") { const health = healthResult.value; const healthy = health.ok !== false && health.status !== "error"; setBackendStatus(healthy && stateResult.status === "fulfilled" ? "online" : "degraded"); } else if (stateResult.status === "rejected") { setBackendStatus("offline"); } if (stateResult.status === "rejected" && reportErrors) { setError(messageFor(stateResult.reason)); } }, [acceptState]); const run = useCallback( async (action: PendingAction, operation: () => Promise) => { setPendingAction(action); setError(null); try { const nextState = await operation(); if (mounted.current) acceptState(nextState); return true; } catch (operationError) { if (mounted.current) { setError(messageFor(operationError)); if (operationError instanceof ApiError && operationError.status === 0) { setBackendStatus("offline"); } } return false; } finally { if (mounted.current) setPendingAction(null); } }, [acceptState], ); const scan = useCallback( () => run("scan", () => api.scanBle({ duration_seconds: 6 })), [run], ); const connect = useCallback( (request: ConnectRequest) => run("connect", () => api.connect(request)), [run], ); const startLive = useCallback( (request: LiveRequest = {}) => run("live", () => api.startLive(request)), [run], ); const startReplay = useCallback( (request: ReplayRequest) => run("replay", () => api.startReplay(request)), [run], ); const stop = useCallback( () => run("stop", () => api.stopSession()), [run], ); useEffect(() => { mounted.current = true; void refresh(true); const poll = window.setInterval(() => void refresh(false), 4_000); return () => { mounted.current = false; window.clearInterval(poll); }; }, [refresh]); useEffect(() => { let dispose: (() => void) | undefined; let retry: number | undefined; let cancelled = false; const connectEvents = () => { if (cancelled) return; dispose = openEventSocket(acceptState, (status) => { if (cancelled) return; setEventStatus(status); if ((status === "closed" || status === "error") && retry === undefined) { retry = window.setTimeout(() => { retry = undefined; connectEvents(); }, 3_000); } }); }; connectEvents(); return () => { cancelled = true; if (retry !== undefined) window.clearTimeout(retry); dispose?.(); }; }, [acceptState]); useEffect(() => { const latency = measuredLatency(state); if (latency === null) return; setLatencyHistory((values) => [...values.slice(-23), latency]); }, [state]); return { state, backendStatus, eventStatus, pendingAction, error, latencyHistory, refresh: () => refresh(true), clearError: () => setError(null), scan, connect, startLive, startReplay, stop, }; }