From 1a2490e2920b9ca77596655daa193c85bde157ea Mon Sep 17 00:00:00 2001 From: stqfdyr <89149493+stqfdyr@users.noreply.github.com> Date: Tue, 6 Oct 2026 21:20:47 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E7=A6=BB=E7=BA=BF=E6=97=B6=E9=95=BF?= =?UTF-8?q?=E8=AF=BB=20hub=20=E7=BB=99=E7=9A=84=20last=5Fseen=5Fago?= =?UTF-8?q?=EF=BC=8C=E6=B5=8F=E8=A7=88=E5=99=A8=E6=97=B6=E9=92=9F=E4=B8=8D?= =?UTF-8?q?=E5=87=86=E6=97=B6=E7=85=A7=E5=B8=B8=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/NodeCard.tsx | 4 ++-- src/lib/api.ts | 2 ++ src/lib/format.test.ts | 7 ++++++- src/lib/format.ts | 11 +++++++++++ 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/components/NodeCard.tsx b/src/components/NodeCard.tsx index 211fd9e..d0b4312 100644 --- a/src/components/NodeCard.tsx +++ b/src/components/NodeCard.tsx @@ -9,7 +9,7 @@ import { Badge } from "@/components/ui/badge" import { Card } from "@/components/ui/card" import { Meter } from "@/components/Meter" import type { Node } from "@/lib/api" -import { bytes, daysUntil, FOREVER, osName, pair, percent, rate, uptime } from "@/lib/format" +import { bytes, daysUntil, FOREVER, osName, pair, percent, rate, sinceSeen, uptime } from "@/lib/format" import { cn } from "@/lib/utils" // Emitted as files and fetched on first use, so a page carries only the flags its @@ -69,7 +69,7 @@ function deployed(node: Node) { * durations, so the badge keeps its shape either way. */ export function Status({ node }: { node: Node }) { - const down = node.last_seen ? Date.now() / 1000 - node.last_seen : 0 + const down = sinceSeen(node) const label = node.online ? `在线 ${node.metrics ? uptime(node.metrics.uptime) : ""}` : deployed(node) diff --git a/src/lib/api.ts b/src/lib/api.ts index c305b85..43ded08 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -32,6 +32,8 @@ export type Node = { /** Set by the operator; empty is ungrouped. Absent from a hub predating groups. */ group?: string last_seen: number + /** Seconds since `last_seen` on the hub's clock, null for a node never seen. Absent on older hubs. */ + last_seen_ago?: number | null metrics: Metrics | null os: string kernel: string diff --git a/src/lib/format.test.ts b/src/lib/format.test.ts index 10f1b89..91f55d0 100644 --- a/src/lib/format.test.ts +++ b/src/lib/format.test.ts @@ -5,7 +5,7 @@ // Nothing imports it, so the bundle never includes it. import { axisBytes, axisTop, bytes, cpuName, cycle, daysUntil, despike, money, osName, pair, percent, quarters, - RATE_FLOOR, rateAxis, tickClock, timeTicks, uptime, windows, withGaps, + RATE_FLOOR, rateAxis, sinceSeen, tickClock, timeTicks, uptime, windows, withGaps, } from "./format.ts" let failed = 0 @@ -136,6 +136,11 @@ eq(RATE_FLOOR, 1024, "底是 1 KB/s") eq(windows(1).map((w) => w.label), ["1 小时", "6 小时", "1 天"], "一天") } +// sinceSeen: the hub's count wins, null included; only its absence falls back to the browser's. +eq(sinceSeen({ last_seen: 1, last_seen_ago: 120 }), 120, "按 hub 的时钟") +eq(sinceSeen({ last_seen: 0, last_seen_ago: null }), 0, "hub 说从未上报") +eq(Math.round(sinceSeen({ last_seen: Date.now() / 1000 - 300 })), 300, "旧 hub 由浏览器计算") + // daysUntil: whole days, negative once past, null when there is no date. { const at = (days: number) => { diff --git a/src/lib/format.ts b/src/lib/format.ts index dc40680..91a5a66 100644 --- a/src/lib/format.ts +++ b/src/lib/format.ts @@ -365,3 +365,14 @@ function median(values: number[]): number { const half = sorted.length >> 1 return sorted.length % 2 ? sorted[half] : (sorted[half - 1] + sorted[half]) / 2 } + +/** + * Seconds since the node last reported, as the hub counts them: subtracted on + * the visitor's clock, a browser eight hours fast would show a node that + * dropped a minute ago as offline for eight hours. The browser's clock serves + * only a hub from before `last_seen_ago`. + */ +export function sinceSeen(node: { last_seen: number; last_seen_ago?: number | null }): number { + if (node.last_seen_ago !== undefined) return node.last_seen_ago ?? 0 + return node.last_seen ? Date.now() / 1000 - node.last_seen : 0 +} From 88ebb63f6e725152985f626e61f4ee33bb90bfa9 Mon Sep 17 00:00:00 2001 From: stqfdyr <89149493+stqfdyr@users.noreply.github.com> Date: Tue, 6 Oct 2026 23:04:59 +0800 Subject: [PATCH 2/2] =?UTF-8?q?test:=20null=20=E7=9A=84=20last=5Fseen=5Fag?= =?UTF-8?q?o=20=E4=B8=8D=E9=80=80=E5=9B=9E=E6=B5=8F=E8=A7=88=E5=99=A8?= =?UTF-8?q?=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/format.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/format.test.ts b/src/lib/format.test.ts index 91f55d0..7cd2696 100644 --- a/src/lib/format.test.ts +++ b/src/lib/format.test.ts @@ -138,7 +138,7 @@ eq(RATE_FLOOR, 1024, "底是 1 KB/s") // sinceSeen: the hub's count wins, null included; only its absence falls back to the browser's. eq(sinceSeen({ last_seen: 1, last_seen_ago: 120 }), 120, "按 hub 的时钟") -eq(sinceSeen({ last_seen: 0, last_seen_ago: null }), 0, "hub 说从未上报") +eq(sinceSeen({ last_seen: Date.now() / 1000 - 300, last_seen_ago: null }), 0, "hub 说从未上报,不改用浏览器计算") eq(Math.round(sinceSeen({ last_seen: Date.now() / 1000 - 300 })), 300, "旧 hub 由浏览器计算") // daysUntil: whole days, negative once past, null when there is no date.