import { useEffect, useState } from 'react' import type { Page } from '@shared/api/types' import { api } from '@shared/api/client' import { openLive } from '@shared/api/source' /** 展示页能拿到的主机状态:只有汇总的数字,没有型号和 IP。 */ export interface Host { cpu_percent: number | null mem_used: number | null mem_total: number | null disk_used: number | null disk_total: number | null net_rx_total: number | null net_tx_total: number | null net_rx_bps: number | null net_tx_bps: number | null net_link_mbps: number | null uptime_seconds: number | null at: string } export interface HostItem { agent_id: string name: string online: boolean stale: boolean last_seen_at: string | null host: Host | null } /** * Agent 主机状态的实时订阅:先用 REST 取一次全量,再用 SSE(`hosts` 主题)接收每一次更新。 * 连接断开时浏览器会自动重连;重连之后服务端会先推一份当前快照。 */ export function useHosts(): { hosts: Map; live: boolean } { const [hosts, setHosts] = useState>(new Map()) const [live, setLive] = useState(false) useEffect(() => { let alive = true api .get>('/api/public/hosts') .then((page) => alive && setHosts(new Map(page.items.map((i) => [i.agent_id, i])))) .catch(() => undefined) const source = openLive('/api/public/live?topics=hosts,nodes') source.onopen = () => setLive(true) source.onerror = () => setLive(false) source.addEventListener('host', (event) => { const data = JSON.parse((event as MessageEvent).data) as { agent_id: string; online: boolean; host: Host } setHosts((previous) => { const next = new Map(previous) const old = previous.get(data.agent_id) next.set(data.agent_id, { agent_id: data.agent_id, name: old?.name ?? data.agent_id, online: data.online, stale: false, last_seen_at: data.host.at, host: data.host }) return next }) }) source.addEventListener('node', (event) => { const data = JSON.parse((event as MessageEvent).data) as { id: string; online: boolean } setHosts((previous) => { const old = previous.get(data.id) if (!old) return previous const next = new Map(previous) next.set(data.id, { ...old, online: data.online, stale: data.online ? old.stale : true }) return next }) }) return () => { alive = false source.close() } }, []) return { hosts, live } } export function fmtBytes(v: number | null | undefined): string { if (v === null || v === undefined) return '—' const units = ['B', 'KB', 'MB', 'GB', 'TB'] let value = v let i = 0 while (value >= 1024 && i < units.length - 1) { value /= 1024 i++ } return `${value.toFixed(value >= 100 || i === 0 ? 0 : 2)} ${units[i]}` } export function fmtSpeed(v: number | null | undefined): string { return v === null || v === undefined ? '—' : `${fmtBytes(v)}/s` } export function fmtUptime(seconds: number | null | undefined): string { if (seconds === null || seconds === undefined) return '—' const d = Math.floor(seconds / 86400) const h = Math.floor((seconds % 86400) / 3600) const m = Math.floor((seconds % 3600) / 60) return d > 0 ? `${d} 天 ${h} 小时 ${m} 分` : h > 0 ? `${h} 小时 ${m} 分` : `${m} 分 ${Math.floor(seconds % 60)} 秒` } export const pct = (used: number | null, total: number | null): number | null => (used !== null && total !== null && total > 0 ? (100 * used) / total : null)