feat: 赛事项目切换 + 记住上次打开的赛事(修「导入赛事包后没数据」)
根因:服务端启动只认 -project(缺省 default),Console 从不传;导入只是把包解到
event_projs/<名字>,没有任何切换入口 → 导入的赛事永远打不开,页面看到的是空的 default。
- 服务端 -project 缺省改为空串;解析顺序:显式 flag(并写回记忆)> <dataDir>/active_project.txt
> default;backup/passwd 口径一致;启动日志打印生效项目与来源
- GET /api/projects(admin)列出各赛事(是否当前/是否有 event.db/大小/更新时间)
- POST /api/projects/activate(admin):校验后原子写记忆文件 → 审计 → 返回
{ok,name,restarting:true} → 触发优雅关闭;Console 对非用户发起的退出会自动拉起,
重启后即打开新赛事(无需改 Console)。本来就是当前赛事则 restarting:false 不重启
- 管理端「导出」页新增赛事项目列表与「切换为当前赛事」;导入成功后提示「已导入为 X,
但还不是当前赛事」并给「立即切换并重启服务端」;切换后轮询 /api/health 等它回来再刷新
- 文档:使用手册(导入≠切换、生效四步)、常见问题(导入后看不到数据)
自检:服务端 12 包全绿;前端 vue-tsc + build、node --test 66/66;console 14 文件通过。
This commit is contained in:
1 parent
005b51b2ec
commit
2d207fd654
14 files changed
+1363
-24
No files matched your search
@@ -641,6 +641,54 @@ export const projectApi = {
|
||||
},
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 切换赛事后的重启等待
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface ServerRestartWaitOptions {
|
||||
/** 健康检查;不抛错即视为服务端已回来。 */
|
||||
probe: () => Promise<unknown>
|
||||
/** 两次探测的间隔,默认 1.5 秒。 */
|
||||
intervalMs?: number
|
||||
/** 总等待上限,默认 60 秒。 */
|
||||
timeoutMs?: number
|
||||
/**
|
||||
* true 表示这次切换会让服务端重启。服务端先优雅退出、再由 Console 拉起,
|
||||
* 旧进程在退出前仍能满足健康检查,所以必须等到探测到一次失败才认为重启真的开始,
|
||||
* 否则会在旧进程上误判「已经回来了」。
|
||||
*/
|
||||
waitForDown?: boolean
|
||||
/** 便于测试注入;默认用 window.setTimeout。 */
|
||||
sleep?: (ms: number) => Promise<void>
|
||||
now?: () => number
|
||||
}
|
||||
|
||||
/** 轮询健康检查等服务端回来;返回 false 表示超时,调用方需提示人工检查服务状态。 */
|
||||
export async function waitForServerOnline(options: ServerRestartWaitOptions): Promise<boolean> {
|
||||
const intervalMs = options.intervalMs ?? 1500
|
||||
const timeoutMs = options.timeoutMs ?? 60_000
|
||||
const now = options.now ?? ((): number => Date.now())
|
||||
const sleep =
|
||||
options.sleep ??
|
||||
((ms: number): Promise<void> =>
|
||||
new Promise((resolve) => {
|
||||
window.setTimeout(resolve, ms)
|
||||
}))
|
||||
const deadline = now() + timeoutMs
|
||||
let downSeen = false
|
||||
for (;;) {
|
||||
try {
|
||||
await options.probe()
|
||||
if (options.waitForDown !== true || downSeen) return true
|
||||
} catch {
|
||||
// 重启窗口内连不上是预期结果,继续等。
|
||||
downSeen = true
|
||||
}
|
||||
if (now() >= deadline) return false
|
||||
await sleep(intervalMs)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 规则包(YAML)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user