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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ MONITOR_HUB=https://hub.example.com npm run dev

构建产物位于 `dist/`。提交前运行 `npm run build && npm run lint && npm test`。

`npm test` 校验数字格式化和实时指标的输入边界。没有测试框架,Node 自己剥掉
`npm test` 校验数字格式化、坐标轴与时间刻度,以及实时指标的输入边界。没有测试框架,Node 自己剥掉
类型,失败时退出码非零。

## 主题包
Expand Down Expand Up @@ -56,7 +56,7 @@ MONITOR_HUB=https://hub.example.com npm run dev

| 接口 | 用途 |
|---|---|
| `GET /api/me` | 站点名、登录状态、公开页开关 |
| `GET /api/me` | 站点名、登录状态、公开页开关、历史保留天数 |
| `GET /api/nodes` | 节点列表、实时指标和累计流量 |
| `GET /api/nodes/{id}/metrics` | 历史指标和延迟记录 |
| `GET /api/ws` | 每 2 秒推送一次节点快照的 WebSocket |
Expand All @@ -66,8 +66,8 @@ MONITOR_HUB=https://hub.example.com npm run dev

`metrics` 的三个查询参数都可省:

- `hours=N` 窗口宽度。**匿名上限 168,登录后 2160**,超出静默 clamp——降采样限的是响应行数,这个
上限限的是 hub 扫描多少行
- `hours=N` 窗口宽度。**上限是 hub 的保留天数**(`/api/me` 的 `history_days`),登录与匿名相同,超出
静默 clamp,所以档位要按它生成。7 天以内取分钟明细,更宽取每小时的汇总
- `points=W` 调用方画得下的点数,只会让 hub 抽得更稀,不会更密
- `series=metrics|ping` 只取要画的那一半,省掉的那半原本占响应的三分之一到三分之二

Expand Down
12 changes: 1 addition & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@
"recharts": "^3.10.1",
"simple-icons": "^16.32.0",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.3",
"tw-animate-css": "^1.4.0"
"tailwindcss": "^4.3.3"
},
"devDependencies": {
"@types/node": "^24.13.3",
Expand Down
9 changes: 6 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import { loadConfig } from "@/lib/config"
type Me = { authed: boolean; github: boolean; site_name: string; public_page: boolean; history_days?: number }

// Split out because recharts is most of this bundle and the list page draws no
// chart. The landing page is 242 kB rather than 629 kB (77 kB gzipped against
// 188 kB), with the rest fetched immediately after it paints.
// chart. The landing page is 288 kB rather than 678 kB (95 kB gzipped against
// 207 kB), with the rest fetched immediately after it paints.
const loadDetail = () => import("@/components/NodeDetail").then((m) => ({ default: m.NodeDetail }))
const NodeDetail = lazy(loadDetail)

Expand All @@ -35,7 +35,10 @@ function useNodeRoute() {
return [
id,
(next: number | null) => {
history.pushState({}, "", next === null ? "/" : `/node/${next}`)
const path = next === null ? "/" : `/node/${next}`
// The site name leads to the list from the list as well, and each repeat
// would otherwise cost one more press of back.
if (location.pathname !== path) history.pushState({}, "", path)
setId(next)
scrollTo(0, 0)
},
Expand Down
5 changes: 3 additions & 2 deletions src/components/Meter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ type Props = { label: ReactNode; pct: number | null; foot: ReactNode; empty?: Re
export function Meter({ label, pct, foot, empty = "—" }: Props) {
// null means the metric has no ceiling to fill, so the bar stays empty rather
// than reporting 0%. What replaces the percentage depends on the reason:
// unknown for a node with no metrics, ∞ for a plan with no limit.
// unknown for a node with no metrics, ∞ for a plan with no limit. The bar stops
// at full while the figure does not, so a plan past its quota reads 209%.
const filled = pct === null ? 0 : Math.min(100, Math.max(0, pct))
return (
<div className="min-w-0">
<div className="flex items-baseline justify-between gap-2">
<span className="truncate text-xs text-muted-foreground">{label}</span>
<span className="tnum text-xs font-medium">
{pct === null ? empty : `${filled < 10 ? filled.toFixed(1) : filled.toFixed(0)}%`}
{pct === null ? empty : `${pct < 10 ? pct.toFixed(1) : pct.toFixed(0)}%`}
</span>
</div>
<div className="mt-1.5 h-1.5 w-full overflow-hidden rounded-full bg-muted">
Expand Down
3 changes: 1 addition & 2 deletions src/components/NodeCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ export function Status({ node }: { node: Node }) {
// Muted once it stops reporting: the figures on the page are genuine, merely
// no longer current.
<Badge
variant="outline"
className={cn("tnum shrink-0 gap-1.5 font-normal", !node.online && "text-muted-foreground")}
>
<span
Expand All @@ -99,7 +98,7 @@ export function Country({ node }: { node: Node }) {
const src = FLAGS[node.country]
if (!src) {
return (
<Badge variant="outline" className="shrink-0 font-normal text-muted-foreground">
<Badge className="shrink-0 font-normal text-muted-foreground">
{node.country}
</Badge>
)
Expand Down
Loading
Loading