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

- 曲线空态和图共用一个定高容器,数据到来不再挤下面的内容
- 路由行去掉行间分割线:它会吃掉 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
+19
View File
@@ -141,6 +141,25 @@ export async function selectProxy(
}
}
/**
* 让内核重启进程(POST /restart,内核源码 hub/route/restart.go:20-43)。
*
* 内核先回 {"status":"ok"} 再自我 exec。Linux 上用的是 syscall.Exec,PID 不变,
* systemd 托管不受影响;策略组的选中靠 profile.store-selected 恢复。
*/
export async function restartKernel(env: Env): Promise<ValidateResult> {
try {
const res = await callApi(env, '/restart', {
method: 'POST',
signal: AbortSignal.timeout(10_000),
})
if (res.ok) return { ok: true, message: '' }
return { ok: false, message: (await res.text()) || `管理接口返回 ${String(res.status)}` }
} catch (error) {
return { ok: false, message: `连不上管理接口:${String(error)}` }
}
}
/**
* 透传一个管理接口的 GET 请求。
*
+9 -1
View File
@@ -12,7 +12,7 @@
import listenerFields from '../shared/listener-fields.json' with { type: 'json' }
import { applyConfig, listBackups, readConfig, restoreBackup } from './config-store.ts'
import { hostInfo, probeExit } from './host.ts'
import { kernelVersion, proxyGet, proxyWrite, selectProxy } from './kernel.ts'
import { kernelVersion, proxyGet, proxyWrite, restartKernel, selectProxy } from './kernel.ts'
import { tokenFromRequest, tokenMatches } from './auth.ts'
import type { ConfigObject } from '../vendor/config-editor/index.ts'
import type { Env } from './env.ts'
@@ -214,6 +214,14 @@ function makeRoute(env: Env, token: string) {
: json({ message: upstream.message || '内核拒绝了这次切换' }, 422)
}
// 重启内核进程。运行时动作,不碰配置文件;和其它写操作一样单独点名,不开通用口子
if (path === '/api/kernel/restart' && req.method === 'POST') {
const upstream = await restartKernel(env)
return upstream.ok
? json({ ok: true })
: json({ message: upstream.message || '内核拒绝了重启请求' }, 502)
}
if (path.startsWith('/api/kernel/') && (req.method === 'PUT' || req.method === 'DELETE')) {
const target = path.slice('/api/kernel'.length)
const rule = WRITABLE.find((w) => w.method === req.method && w.pattern.test(target))