Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/components/NodeCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion src/lib/format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: 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.
{
const at = (days: number) => {
Expand Down
11 changes: 11 additions & 0 deletions src/lib/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}