Files
MoShiro/web/display/src/live.ts
T

102 lines
3.5 KiB
TypeScript

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<string, HostItem>; live: boolean } {
const [hosts, setHosts] = useState<Map<string, HostItem>>(new Map())
const [live, setLive] = useState(false)
useEffect(() => {
let alive = true
api
.get<Page<HostItem>>('/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<string>).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<string>).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)