"use client" import { useEffect, useState } from "react" import { resolveApiUrl, withAuthHeaders } from "@/shared/api/http-client" export interface TrafficLiveSample { rxMbps: number txMbps: number at: string } function parseSseBlock(block: string): { event: string; data: string } { let event = "message" const dataLines: string[] = [] for (const line of block.split("\n")) { if (line.startsWith("event:")) event = line.slice(6).trim() else if (line.startsWith("data:")) dataLines.push(line.slice(5).trim()) } return { event, data: dataLines.join("\n") } } export function useTrafficLive(opts: { enabled: boolean backendUrl: string serverId: string iface: string }): { sample: TrafficLiveSample | null; error: string | null } { const [sample, setSample] = useState(null) const [error, setError] = useState(null) useEffect(() => { if (!opts.enabled || !opts.serverId) { setSample(null) setError(null) return } const ac = new AbortController() setSample(null) setError(null) const ifaceQ = opts.iface && opts.iface !== "__all__" ? `?iface=${encodeURIComponent(opts.iface)}` : "" const path = `/api/traffic/servers/${encodeURIComponent(opts.serverId)}/live${ifaceQ}` const url = resolveApiUrl(opts.backendUrl, path) let buf = "" setError(null) void (async () => { try { const res = await fetch(url, { headers: withAuthHeaders({ Accept: "text/event-stream" }), signal: ac.signal, credentials: "include", }) if (!res.ok || !res.body) { setError(`live HTTP ${res.status}`) return } const reader = res.body.getReader() const decoder = new TextDecoder() while (!ac.signal.aborted) { const { done, value } = await reader.read() if (done) break buf += decoder.decode(value, { stream: true }) const parts = buf.split("\n\n") buf = parts.pop() ?? "" for (const raw of parts) { if (!raw.trim() || raw.trim().startsWith(":")) continue const ev = parseSseBlock(raw) if (ev.event === "sample" && ev.data) { const parsed = JSON.parse(ev.data) as TrafficLiveSample setSample(parsed) setError(null) } else if (ev.event === "error" && ev.data) { const parsed = JSON.parse(ev.data) as { error?: string } setError(parsed.error ?? "live error") } } } } catch (e) { if (ac.signal.aborted) return setError(e instanceof Error ? e.message : "live error") } })() return () => ac.abort() }, [opts.enabled, opts.backendUrl, opts.serverId, opts.iface]) return { sample, error } }