反馈改右上角轻提示;侧栏加撤回重做等配置动作

设置页的横幅换成 toast(右滑入、几秒后上移淡出,web/toast.ts 单例):
动作结果看一眼就行,不值得在页面里占一块常驻位置。表单校验类的错误
仍留在各自表单里。

撤回栈就是备份链:撤回把最新备份应用回去并消费掉它,撤回前的状态进
重做栈(.patchbay-redo,同样留 10 份);重做反向;新的正常保存清空
重做栈,不留分叉历史。这两条路复用 applyConfig 的保护链路,用
restoreFrom 参数跳过自建备份——否则备份链会被撤回本身污染,撤两次
等于原地打转。侧栏左下角一排图标:撤回、重做、历史记录(备份列表
弹窗)、更新配置(POST /api/kernel/reload,给 SSH 手改后用)、重启
内核;收拢时容器变窄自动折成一列。

设置页顺手修三处:GEO 行的编辑按钮在输入框开着时再点会闪(blur 关了
click 又开),mousedown.prevent 保焦点、click 显式开关;上传按钮改
「选择或拖放文件」,投放区拖入高亮;恢复策略挪到恢复行下面。
This commit is contained in:
2026-08-12 01:59:22 +08:00
parent 2580c03bfa
commit f761d13579
9 changed files with 798 additions and 106 deletions
+52 -1
View File
@@ -2,7 +2,16 @@ import { afterAll, describe, expect, it } from 'bun:test'
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { applyConfig, importConfig, listBackups, readConfig, restoreBackup } from './config-store.ts'
import {
applyConfig,
importConfig,
listBackups,
listRedo,
readConfig,
redoConfig,
restoreBackup,
undoConfig,
} from './config-store.ts'
import type { Env } from './env.ts'
/**
@@ -184,6 +193,48 @@ describe('落盘保护链路(内核在线)', () => {
if (!result.ok) expect(result.message).toContain('没有这个备份')
})
it('撤回回到改动前,重做拿回改动,备份链不被撤回污染', async () => {
const env = makeEnv()
const first = await readConfig(env)
await applyConfig(env, { ...first.data, 'mixed-port': 7891 }, first.revision)
const undone = await undoConfig(env)
expect(undone.ok).toBe(true)
expect(readFileSync(env.configPath, 'utf8')).toContain('mixed-port: 7890')
// 备份被消费、当前状态进了重做栈
expect(await listBackups(env)).toHaveLength(0)
expect(await listRedo(env)).toHaveLength(1)
const redone = await redoConfig(env)
expect(redone.ok).toBe(true)
expect(readFileSync(env.configPath, 'utf8')).toContain('mixed-port: 7891')
expect(await listRedo(env)).toHaveLength(0)
expect(await listBackups(env)).toHaveLength(1)
})
it('栈空时撤回和重做被拦住,文件不动', async () => {
const env = makeEnv()
const undone = await undoConfig(env)
expect(undone.ok).toBe(false)
if (!undone.ok) expect(undone.message).toContain('没有可以撤回')
const redone = await redoConfig(env)
expect(redone.ok).toBe(false)
expect(readFileSync(env.configPath, 'utf8')).toBe(ORIGINAL)
})
it('新的保存清空重做栈', async () => {
const env = makeEnv()
const first = await readConfig(env)
await applyConfig(env, { ...first.data, 'mixed-port': 7891 }, first.revision)
await undoConfig(env)
expect(await listRedo(env)).toHaveLength(1)
const { data, revision } = await readConfig(env)
await applyConfig(env, { ...data, 'mixed-port': 7892 }, revision)
expect(await listRedo(env)).toHaveLength(0)
})
it('备份最多留 10 份,旧的被清掉', async () => {
const env = makeEnv()
const dir = join(env.configPath, '..', '.patchbay-backups')