仪表盘细节修整,内核卡片搬左列并加重启

- 曲线空态和图共用一个定高容器,数据到来不再挤下面的内容
- 路由行去掉行间分割线:它会吃掉 hover 高亮的圆角
- 每路走势线先合并成 28 桶再画。3 秒一个采样直接全画,在一条
  百来像素宽的线上只剩噪声;没采集的桶画断口,不画假零
- 环形图两段加圆角,交界用底色描边让出间隙
- 内核卡片从右列搬到服务器和路由之间(左列本来就矮一截),加
  「重启内核」:POST /restart 后内核自我 exec、PID 不变,恢复没有
  任何通知,只能每 2 秒探一次,半分钟探不到交还给外壳心跳
This commit is contained in:
2026-08-11 22:42:40 +08:00
parent eec5b791c8
commit 07a43b5a56
6 changed files with 248 additions and 54 deletions
+31
View File
@@ -1,5 +1,6 @@
import { describe, expect, test } from 'bun:test'
import {
bucketRates,
lastRate,
pushWindow,
routeDeltas,
@@ -79,6 +80,36 @@ describe('routeDeltas', () => {
})
})
describe('bucketRates', () => {
test('样本按桶平均,桶的时间戳在桶中央', () => {
const samples: RouteSample[] = [
{ t: 1_000, ms: 1_000, up: 100, down: 0 },
{ t: 2_000, ms: 1_000, up: 300, down: 0 },
{ t: 6_000, ms: 1_000, up: 50, down: 50 },
]
// 0-10s 分两桶:前桶 400 字节 / 2 秒 = 200 B/s,后桶 100 字节 / 1 秒 = 100 B/s
expect(bucketRates(samples, 0, 10_000, 2)).toEqual([
[2_500, 200],
[7_500, 100],
])
})
test('开头没数据的桶被裁掉,中间的留成 null', () => {
const samples: RouteSample[] = [
{ t: 3_000, ms: 1_000, up: 100, down: 0 },
{ t: 9_000, ms: 1_000, up: 100, down: 0 },
]
const points = bucketRates(samples, 0, 10_000, 5)
expect(points).toHaveLength(4)
expect(points[0]).toEqual([3_000, 100])
expect(points[1]![1]).toBeNull()
})
test('没有任何样本返回空数组', () => {
expect(bucketRates([], 0, 10_000, 5)).toEqual([])
})
})
describe('windowBytes / lastRate', () => {
const samples: RouteSample[] = [
{ t: 0, ms: 3_000, up: 100, down: 100 },