机架管理前后端(rack/):FastAPI + SQLite + Vue3,经统一门户上线

- 后端 prrack:MQTT(phonerail-backend @ 127.0.0.1:1884)↔ 遥测入库 / 事件增量同步 / WebSocket 推送;
  命令白名单 + 重传同 seq;审计记门户 X-Auth-User(只绑 127.0.0.1,main.py 拒绝非回环)。
- 自动恢复:只看电流(平线 / 过低,服务器够不到手机 adb),先长按电源再断电重启,
  每小时限次后停手;默认逐台关闭。adb 探活接口 recovery.Prober 预留。
- 前端:总览 / 板子页(五路操作、电流电压曲线)/ 事件 / 操作记录 / 自动恢复;手机宽度可用。
- 部署 deploy/deploy.sh:服务器 Python 3.8 上先跑测试再替换;systemd phonerail-rack,每日备份。
- 门户应用 id 16、key rack(phonerail 已被需求文档站占用),授权 admin + staff。

测试 26 例(本机 3.14 / 服务器 3.8 均过)。线上板 b81f3fc9c8bc 在线,WS 经门户 101 + 1 条/s 遥测。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
zhou yang
2026-09-27 12:33:28 +08:00
共同撰写人 Claude Opus 5.5
父节点 a32fa06c43
当前提交 26c04ce878
共修改 34 个文件,包含 4031 行新增和 0 行删除
+66
查看文件
@@ -0,0 +1,66 @@
# PhoneRail 机架管理(服务器前后端)
需求文档 D7 的实现:板子经 WiFi → MQTT(TLS)连到 home 服务器,这里把遥测存下来、画出来,
让人在网页上操作每一台手机,并按电流判据做自动崩溃恢复(先软后硬)。
```
板子 ──mqtts 8883──▶ mosquitto(evse 共用)──127.0.0.1:1884──▶ 后端 prrack(127.0.0.1:7764)
│ SQLite(WAL)
浏览器 ──▶ rack.home.example:7755(统一门户,auth_request)──────────┘ 同进程托管前端 dist
```
| 目录 | 内容 |
|---|---|
| `backend/prrack/` | `protocol.py` 板侧协议镜像(命令白名单 / 事件表 / seq)· `bus.py` MQTT 抽象(paho / 进程内假 broker)· `hub.py` 核心 · `recovery.py` 自动恢复 · `db.py` 存储 · `api.py` HTTP+WS · `simboard.py` 模拟板子 · `main.py` 入口 |
| `backend/tests/` | 25 例(pytest),本机与服务器(Python 3.8)都跑 |
| `frontend/` | Vue3 + Vite + Element Plus + ECharts(按需引入) |
| `deploy/` | `deploy.sh` 一键部署 · systemd unit · 每日备份 |
## 规矩(都来自板侧协议 §10.8,改代码前先看)
1. **重传用同一个 seq**(`Hub.call`)。换 seq 重发 = 「按 10 秒电源键」可能执行两遍。测试 `test_retransmit_reuses_the_same_seq…` 钉着。
2. **失联不动作**:板子离线 / 遥测中断只标状态;自动恢复只在「开了 + 在线 + 在位 + 有电 + 执行器空闲」时判定。
3. **时间用服务器收到的时刻**(板子没有墙钟),事件表另存板子的 `t_ms` 作参考。
4. **只绑 127.0.0.1**:`X-Auth-User`(门户覆盖式注入)按 L2 信任,只用于审计「谁按的」;`main.py` 拒绝非回环绑定。
5. **命令白名单在 `protocol.build_command`**:`wifi` / `mqtt`(只收 USB)与 `events`(服务器自己同步)不给界面。板子的校验仍是唯一权威。
## 本机开发
```bash
cd backend && python -m prrack.main --sim 2 --db dev.db --static ../frontend/dist # 两块模拟板,第 1 块第 2 路「卡死」
cd frontend && pnpm dev # http://localhost:5173,/api 反代到 7764
cd backend && python -m pytest -q tests
```
## 部署 / 运维
```bash
bash rack/deploy/deploy.sh # 构建 → 传 → 服务器上 Python 3.8 跑测试(不过不换)→ 重启
```
| 项 | 值 |
|---|---|
| 服务 | systemd `phonerail-rack`,`/root/phonerail-rack/{backend,frontend/dist,venv,data}` |
| 日志 | `/var/log/phonerail-rack.log` |
| 库 | `/root/phonerail-rack/data/rack.db`;每日 03:40 `sqlite3 .backup` 到 `backup/`,留 14 份 |
| MQTT | `phonerail-backend` @ 127.0.0.1:1884,口令读 `/root/phonerail/mqtt.env` |
| 门户 | 应用 id 16,key `rack`、端口 7764(`phonerail` 已被需求文档站占用);授权 admin + staff,**不给 viewer**(没有只读模式,能断电重启) |
| 保留期 | 原始 1 s 遥测 2 天;分钟聚合 90 天;事件 / 审计 / 恢复记录永久 |
加一块新板:USB 配网(`firmware/tools/prlink.py` 发 `wifi` / `mqtt`)+ 服务器上给它开 MQTT 账号(见 memory 里的 broker 说明),上线后自动出现在总览。
## 踩过的坑
- **uvicorn 没装 `websockets` 时不报错**:`/api/ws` 被当成普通 GET 落到 SPA 兜底,回 200 HTML,页面能开但永不刷新。
TestClient 自带 WS 实现测不出来 ⇒ `main.py` 启动自检 + `test_a_real_websocket_library_is_installed`(部署时在服务器上跑)。
- **服务器 Python 3.8 + 老 pip**:starlette 最高 0.44.0、websockets 13.1;新版本在清华镜像上直接「找不到」。
- 本机有 HTTP 代理(`HTTP_PROXY=127.0.0.1:7890`),经它访问不存在的公网端口会回 **502 而不是连不上**;验「端口没暴露」要加 `--noproxy '*'`。
- 门户的 `/admin/grants` 是**整张矩阵覆盖写**:脚本化加授权要先读出现有全部授权再加新的。改门户前先备份 `portal.db`。
- 门户进程把应用表缓存在内存,**直接改库它看不见**(没有重载信号,只能重启);走它自己的 `/admin/apps` 接口会自动 `reloadAcl()`。
## 已知限制 / 下一步
- **自动恢复阈值没有实测依据**(平线 5 mA / 过低 30 mA / 窗口 5 分钟)。插上刀片和手机后先看几天曲线再定,默认逐台关闭。
- **adb 探活**:接口 `recovery.Prober` 已留,服务器目前够不着手机(机架在 192.168.8.x 的 NAT 后面)。
- 前端 Element Plus 是整包引入(gzip ≈335 KB),手机首屏偏慢;改成按需引入可以再砍一半。
- 没有细分权限:门户授权了这个应用的人都能操作;审计记着是谁。