Files
Scoreboard/console/README.md
T
AskaEth 005b51b2ec fix: 数据目录移出安装目录,修掉「覆盖安装/卸载即丢数据」
根因:数据目录是 <安装目录>\data(config.yaml 的 dataDir 相对服务端工作目录),
而 electron-builder 的卸载器结尾执行 RMDir /r $INSTDIR,且覆盖安装会先跑旧卸载器
(installSection.nsh:52 的 uninstallOldVersion)→ 赛事库/规则/用户/审计/上传图片
连同安装目录被整个删除,再装新版。卸载一次同样如此。

- 服务端新增 -data <dir>(主命令、backup、passwd 三处),非空时覆盖 config.yaml 的
  dataDir;不传时行为完全不变(兼容便携/手工运行)
- Console 把数据指到当前用户目录 %LOCALAPPDATA%\ScoreboardConsole\data(卸载器不会碰
  这里;没有用 app.getPath('userData'),因为 %APPDATA%\<AppName> 正是被删的那个)
- 一次性迁移:目标为空且 <安装目录>\data 有数据时整体改名过去;两边都有数据时不动作
  只记警告;任何失败都保留原地数据、绝不删除
- passwd 也带 -data(否则重置的是安装目录下那个空库里的账号)
- deploy/backup.bat 指向同一数据目录
- 文档:部署指南新增「数据放在哪里(升级/卸载都不会删)」+ 备份/恢复/迁移路径改写,
  上线检查清单与使用手册同步

自检:服务端 12 包全绿;console 语法 14 文件 + datadir 单测 11/11;backup.bat 保持纯 ASCII。
2026-09-10 09:17:56 +08:00

175 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Scoreboard Console
赛事积分榜系统的桌面宿主(Electron)。它只做六件事:
1. 拉起并守护同目录(或配置指定)的 `scoreboard.exe`,把它的 stdout/stderr 接管到管理窗口与日志文件;
2. 用 `screen.getAllDisplays()` 枚举显示器,把大屏页面投到扩展屏;
3. `powerSaveBlocker.start('prevent-display-sleep')` 防熄屏;
4. 提供启动/停止/状态/日志的管理 GUI 和托盘菜单;
5. 默认注册登录项但不启用(不随系统启动),可在服务管理窗口的「开机自启」开关里开启;
6. 在服务管理窗口展示服务端在线的大屏实例(`GET /api/display/instances`)。
规范来源:`devdocs/06-大屏端设计.md` 第 6 节、`devdocs/10-部署与现场方案.md` 第 4 节。
## 目录结构
```
console/
├── package.json # main 指向 main/index.js;electron-builder 配置
├── config.json # 运行配置(端口 / exe 路径 / 选屏策略)
├── main/
│ ├── index.js # 应用入口:窗口 / 托盘 / IPC / 生命周期
│ ├── config.js # 配置加载与合并、exe 路径解析
│ ├── datadir.js # 数据目录解析 + 旧数据一次性迁移
│ ├── datadir.test.js # 数据目录的单元测试(node --test main/datadir.test.js)
│ ├── service.js # 服务端子进程守护:spawn / 日志转发 / 健康检查 / 自动重启
│ ├── instances.js # 已连接大屏实例看板:复用管理面板登录态读 /api/display/instances
│ ├── account.js # 管理员账号重置:参数校验 + 调 scoreboard.exe passwd
│ ├── displays.js # 大屏窗口管理:选屏、kiosk、排布、24h 重载
│ ├── log-bus.js # 日志环形缓冲 + 落文件 + 广播
│ └── tray-icon.js # 托盘图标(base64 内嵌,无外部资源文件)
├── scripts/
│ └── check-syntax.js # pnpm check:跨平台递归 node --check
├── extra/ # 打包前由 deploy/build.bat 暂存 使用说明.txt(extraFiles 指向它)
└── renderer/
├── index.html # 管理窗口(含自绘标题栏)
├── styles.css
├── app.js
├── preload.js # 管理窗口的 contextBridge 白名单
└── shell-preload.js # 服务端管理面板窗口的 window.desktopShell 白名单(窗口控制 + 显示器菜单)
```
## 运行方式
```bash
cd console
pnpm install # 首次需要下载 Electron
pnpm start # 等价于 electron .
```
开发时把 `scoreboard.exe` 放在以下任一位置都能被找到(`config.json` 里的相对路径会按顺序探测):
1. `console/`
2. 当前工作目录
3. 仓库根目录
4. `dist/`
现场(部署包)形态:
```
scoreboard-windows-amd64/
├── Scoreboard_Console.exe
├── resources/app.asar # 内含 main/ renderer/ config.json
├── config.json # exe 旁边的可编辑配置
├── scoreboard.exe
├── config.yaml
├── rules/
└── logs/console.log # 运行时生成
```
数据不在安装目录里,默认落在 `%LOCALAPPDATA%\ScoreboardConsole\data`(见下文「数据目录」)。
打包后 `config.json` 读取顺序:`SCOREBOARD_CONSOLE_CONFIG` 环境变量 → exe 同目录的 `config.json`。
## 数据目录(重要)
electron-builder 生成的卸载器结尾是 `RMDir /r $INSTDIR`,而**覆盖安装会先静默跑一遍旧版卸载器**,所以装在安装目录里的 `data\` 会被整体删掉;`%APPDATA%\Scoreboard Console` 卸载时同样会删,因此也不能用 `app.getPath('userData')`。最终方案:
| 场景 | 数据目录 |
| --- | --- |
| 打包态,`server.dataDir` 留空(默认) | `%LOCALAPPDATA%\ScoreboardConsole\data`(卸载器不碰这个位置) |
| 配置了 `server.dataDir` | 配什么用什么:绝对路径直接用;相对路径以安装目录为基准 |
| 开发态(`pnpm start`),`server.dataDir` 留空 | 不接管,沿用服务端默认(工作目录下的 `data`),仓库里的 `data/` 不受影响 |
- Console 把最终目录通过服务端的 `-data <绝对路径>` 传下去,`passwd` 子命令带同一个参数,保证改的是同一个库里的账号;
- 迁移只在**打包态**做一次:**目标目录为空(或不存在)且 `<安装目录>\data` 有数据**时,用 `fs.renameSync` 把旧目录整体改名过去(同盘秒完成)。跨盘、权限不足等任何失败都只记日志,旧数据原地保留;
- 两边都有数据时**不做任何动作**,只写一条醒目日志提示人工确认,两份都不会被自动删除;
- 迁移结果与最终数据目录都写进 `logs/console.log`(搜「数据目录」「已把旧数据从」);
- 数据目录若仍在安装目录内,启动时会额外打一条警告。
改完 `datadir.js` 跑 `node --test main/datadir.test.js`(Node 内置测试运行器,无额外依赖)。
## 行为说明
| 场景 | 行为 |
| --- | --- |
| 只有一块屏 | 窗口化(屏幕 80% 大小、居中),不做多屏编排,**不挂** `display-metrics-changed` 等监听(行为冻结);`F11` 手动切换全屏 |
| 多块屏 | 除内置屏外的每块屏一个 kiosk 窗口,按 `display.bounds`(DIP)定位,窗口 `setIgnoreMouseEvents(true)`,光标由页面 CSS 隐藏 |
| 选屏 | 运行时枚举,不写死坐标;`selection` 可选 `all-secondary`(默认,用 `display.internal` 排除内置屏)/ `all` / `specified` |
| 页面锁定 | 每块屏各锁一页,URL 形如 `http://127.0.0.1:19540/display?page=standings&lock=1&screen=<id>&instance=<host>-<id>`,页序按 `display.pages` 轮转 |
| 防熄屏 | Console 启动即生效,退出时释放;前台后台都算 |
| 服务端就绪 | 首次 `/api/health` 通过后开大屏窗口;等待超过 `display.waitForServerMs` 也先开,避免现场只见桌面 |
| 服务端崩溃 | 按 `restartDelayMs` 递增退避自动重启(`maxRestarts=0` 为无限);持续健康 `healthyResetMs` 后崩溃计数清零;恢复健康后自动重载大屏 |
| 数据目录 | 默认 `%LOCALAPPDATA%\ScoreboardConsole\data`(安装目录之外,覆盖安装/卸载都不删),启动时按 `-data <绝对路径>` 交给服务端;打包态首次启动做一次性迁移(见「数据目录」一节),开发态不接管 |
| 停止服务 | 先 `POST server.shutdownPath` 并带 `X-Shutdown-Token`(启动时用 `crypto.randomBytes(32)` 生成,经环境变量 `SCOREBOARD_SHUTDOWN_TOKEN` 只交给该子进程)请求服务端优雅关闭;受理后最多等 `shutdownTimeoutMs`,超时或接口不可用(404/连接失败)再强杀 |
| 已连接大屏实例 | 管理窗口打开时每 5 秒读一次服务端 `GET /api/display/instances`(复用管理面板的 `scoreboard_session` Cookie,需记录员及以上),显示实例数、页面名与在线状态;窗口隐藏即停表,未登录/服务未启动时显示占位文案 |
| 关管理窗口 | 只隐藏到托盘,服务端继续运行;退出必须走托盘「退出」 |
| 开机自启 | 默认「已注册但未启用」;在服务管理窗口的「开机自启」开关里控制(`startup.openAtLogin`),仅打包后的 Windows/macOS 生效,开发态不注册 |
| 窗口外观 | 管理窗口与服务端管理面板窗口都是 `frame: false` 无边框,标题栏与最小化/最大化/关闭三键由页面自绘(拖拽区 `-webkit-app-region: drag`,按钮 `no-drag`,双击标题栏切最大化,关闭键 hover 变红) |
| 服务端管理面板 | 用独立无边框 Electron 窗口加载 `http://127.0.0.1:<port>/admin`(`shell-preload.js` 暴露 `window.desktopShell`),不再调系统浏览器;面板页在浏览器里直接打开时标题栏自动不渲染 |
| 显示器菜单 | 面板页的「大屏控制 → 显示器」卡片经 `display:list` / `display:open` / `display:close` / `display:relayout` 指挥本机开窗:按屏创建、聚焦、收窗。**全屏只在「多于一块屏 + 非主屏」时放行**,判定在主进程做,页面传 `fullscreen: true` 也会被降级为窗口化;面板页只拿到这几条白名单通道,拿不到 `ipcRenderer` |
| 管理员账号 | 主进程 `spawn` 同目录 `scoreboard.exe passwd -config <同一份配置> -data <同一个数据目录> -user <当前用户名> -new-username <新用户名> -password <新密码>`(留空 = 不改 / 随机生成)。本机进程、不经 HTTP,因此免登录验证;用户名/密码含空格、制表符或换行会被主进程拒绝,密码不进日志。`passwd` 会作废该用户全部会话,UI 提交前二次确认并提示 |
| 日志 | 内存环形缓冲(`log.maxLines`)+ 文件 `logs/console.log`(超 `maxFileBytes` 轮转一份 `.1`,轮转前会删掉旧 `.1` 以兼容 Windows) |
## 配置项(`config.json`)
| 键 | 默认值 | 说明 |
| --- | --- | --- |
| `server.exe` | `scoreboard.exe` | 相对路径按上文顺序探测,也可写绝对路径 |
| `server.args` | `["-config","config.yaml"]` | 传给服务端的参数 |
| `server.dataDir` | `""` | 数据目录;空串 = 用默认位置(打包态 `%LOCALAPPDATA%\ScoreboardConsole\data`),填了就按它来(相对路径以安装目录为基准)。Console 会把它作为 `-data <绝对路径>` 追加到服务端参数后 |
| `server.host` / `server.port` | `127.0.0.1` / `19540` | 健康检查、关闭握手与管理面板地址(`0.0.0.0` 会自动换成回环地址探测) |
| `server.autoStart` | `true` | Console 启动后是否立刻拉起服务端 |
| `server.healthIntervalMs` | `5000` | 健康检查间隔 |
| `server.restartDelayMs` | `3000` | 重启基础退避,每次翻倍,最多 5 倍 |
| `server.maxRestarts` | `0` | 0 = 无限重启 |
| `server.shutdownPath` | `/api/shutdown` | 优雅关闭握手接口(需服务端实现并校验 `X-Shutdown-Token`),不可用时回退强杀 |
| `server.shutdownTimeoutMs` | `5000` | 服务端受理关闭请求后等待其自行退出的上限,超过则强杀 |
| `server.healthyResetMs` | `60000` | 连续健康多久后清零崩溃计数 |
| `display.selection` | `all-secondary` | `all-secondary` / `all` / `specified` |
| `display.displays` | `[]` | `specified` 时生效,填 display id 或 0 起序号 |
| `display.pages` | `["standings","medals","individual","results"]` | 按屏序轮转锁定;只填大屏支持的页名 |
| `display.pageParam` | `page` | 页面 URL 参数名 |
| `display.kiosk` | `true` | 多屏时是否 kiosk(单屏恒窗口化) |
| `display.ignoreMouse` | `true` | 多屏窗口是否忽略鼠标 |
| `display.reloadEveryHours` | `24` | 定时重载兜底,0 = 关闭 |
| `display.waitForServerMs` | `20000` | 服务端未就绪时开大屏的兜底等待 |
| `display.backgroundColor` | `#0b0f1a` | 窗口底色,减少加载白闪 |
| `startup.openAtLogin` | `false` | 默认注册但不启用(任务管理器可见、已禁用);在服务管理窗口「开机自启」开关里控制(开发态不注册) |
| `admin.width` / `admin.height` | `1040` / `760` | 管理窗口初始尺寸(服务端管理面板窗口按 `width` 不小于 1100 取用) |
| `log.maxLines` / `log.file` / `log.maxFileBytes` | `2000` / `logs/console.log` / `4194304` | 日志缓冲与文件 |
配置缺项自动用内置默认值,文件不存在或 JSON 语法错误时回落到全默认值,并在管理窗口顶部给出提示。
## 打包(electron-builder)
`package.json` 里已写好 `build` 段:
```bash
cd console
pnpm install
pnpm check # 跨平台语法检查(scripts/check-syntax.js)
pnpm dist # electron-builder --win --x64 → ../dist/console/
pnpm pack # 只出目录,不生成安装包,便于现场调试
```
- 产物:NSIS 安装包 + zip(`Scoreboard Console-0.1.0-x64.*`)。
- `extraFiles` 会把 `../dist/scoreboard.exe`、`../config.yaml`、`config.json`(exe 旁的可编辑配置,缺失时 Console 会回落到内置默认值)与 `../rules` 拷到安装目录,最终形成第 4 节里的部署包形态。
- `使用说明.txt` 是可选的:`deploy/build.bat` 在打包前依次找 `docs/使用说明.txt`、`docs/使用手册.md`,命中就暂存到 `console/extra/使用说明.txt`(`extraFiles` 会把该目录内容平铺到部署包根目录),都没有就跳过并在构建日志里注明。
- 图标在 `resources/icon.ico`(`package.json` 的 `win.icon` 指向它)。
- 主程序文件名由 `win.executableName` 决定,现在是 `Scoreboard_Console.exe`;`productName` 仍是 `Scoreboard Console`(显示名、`userData` 目录不变)。
- `nsis.include` 指向 `resources/installer-custom.nsh`(构建时由 `deploy/prepare-extra.mjs` 生成,已 gitignore):许可页顶部文案 + 记住上次安装目录(重装时默认沿用,不退回系统默认路径)。
- 已在 Windows 构建机上通过 `deploy\build.bat` 实跑(含 `pnpm dist`,产出 NSIS 安装包 + zip);Linux 开发机未安装 Electron/electron-builder。
## 尚未实机验证
以下内容只做了静态语法检查,需要在 Windows + 真实多显示器环境跑一遍:
- 多屏 kiosk 定位与 `display-metrics-changed` 重排(改分辨率 / 缩放 / 插拔 HDMI)。
- 管理面板显示器菜单:真正的 kiosk 全屏 / 窗口化结果、`screen.getAllDisplays()` 的 `label` 与主屏判定、`display:list` 返回的 scaleFactor 是否与现场一致。
- `powerSaveBlocker` 在长时间后台下的实际效果。
- 托盘图标在 Windows 任务栏的显示效果(16px 缩放)。
- `scoreboard.exe` 真实拉起、日志转发、健康检查与崩溃重启的端到端链路。
- 数据目录迁移实机路径:老版本安装目录里的 `data\` 是否被整体搬到 `%LOCALAPPDATA%\ScoreboardConsole\data`、跨盘安装(安装目录在 D 盘、`%LOCALAPPDATA%` 在 C 盘)时的失败日志与「旧数据原地保留」表现。
- electron-builder 打包已通过 `deploy\build.bat` 实跑;NSIS 安装包在真机上的安装 / 卸载流程尚未验证。