chore: add maintenance workflow
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
# 维护手册
|
||||
|
||||
本项目长期维护时,目标是每次更新都能做到可检查、可部署、可回滚,并且不误改业务数据。
|
||||
|
||||
## 日常更新流程
|
||||
|
||||
1. 查看当前改动:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
```
|
||||
|
||||
2. 修改代码后先跑本地检查:
|
||||
|
||||
```bash
|
||||
make check
|
||||
make smoke
|
||||
```
|
||||
|
||||
3. 部署前记录业务数据哈希:
|
||||
|
||||
```bash
|
||||
make data-hash
|
||||
```
|
||||
|
||||
4. 构建、重启、健康检查和部署后哈希复核:
|
||||
|
||||
```bash
|
||||
make deploy
|
||||
```
|
||||
|
||||
5. 确认容器运行状态:
|
||||
|
||||
```bash
|
||||
make ps
|
||||
```
|
||||
|
||||
## 常用命令
|
||||
|
||||
- `make check`:检查前端 JS 语法和后端 Python 编译。
|
||||
- `make smoke`:运行轻量前端行为烟测,覆盖排序按钮、组内排序和纠错后排序。
|
||||
- `make data-hash`:输出 `classnotes.txt` 和 `学生课时账户.md` 的 SHA-256。
|
||||
- `make build`:构建 Docker 镜像。
|
||||
- `make up`:重启 Docker Compose 服务。
|
||||
- `make health`:带认证访问 `/api/health`。
|
||||
- `make deploy`:按推荐顺序执行检查、构建、重启、健康检查和哈希复核。
|
||||
- `make logs`:查看最近服务日志。
|
||||
|
||||
## 数据保护
|
||||
|
||||
业务数据文件位于:
|
||||
|
||||
```text
|
||||
/root/新时空数据/data/classnotes.txt
|
||||
/root/新时空数据/data/学生课时账户.md
|
||||
```
|
||||
|
||||
普通前端和查询类改动不应该改变这两个文件。部署前后 `make data-hash` 输出应一致;如果涉及登记 API 或数据写入功能,先确认自动备份目录:
|
||||
|
||||
```text
|
||||
/root/新时空数据/data/backups/
|
||||
```
|
||||
|
||||
恢复备份时先停止服务,把备份文件复制回数据目录,再重新启动服务。
|
||||
|
||||
## 回滚建议
|
||||
|
||||
如果部署后页面异常:
|
||||
|
||||
1. 查看容器和日志:
|
||||
|
||||
```bash
|
||||
make ps
|
||||
make logs
|
||||
```
|
||||
|
||||
2. 回到上一个可用 Git 版本后重新部署:
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git log --oneline -5
|
||||
```
|
||||
|
||||
3. 重新执行:
|
||||
|
||||
```bash
|
||||
make deploy
|
||||
```
|
||||
|
||||
如果业务数据哈希异常,先不要继续写入数据,优先从 `../data/backups/` 或外部备份恢复。
|
||||
|
||||
## 提交前检查清单
|
||||
|
||||
- `make check` 通过。
|
||||
- `make smoke` 通过。
|
||||
- 查询类或前端类改动部署前后业务数据哈希一致。
|
||||
- `make health` 正常返回 `ok: true`。
|
||||
- 重要功能改动已在 `README.md` 或本文件补充维护说明。
|
||||
@@ -0,0 +1,40 @@
|
||||
SHELL := /bin/bash
|
||||
|
||||
APP_PORT ?= 18080
|
||||
DATA_FILES := ../data/classnotes.txt ../data/学生课时账户.md
|
||||
|
||||
.PHONY: check smoke data-hash build up ps health deploy logs
|
||||
|
||||
check:
|
||||
node --check app/static/app.js
|
||||
python3 -m py_compile app/main.py app/data.py
|
||||
|
||||
smoke:
|
||||
node scripts/smoke_test.js
|
||||
|
||||
data-hash:
|
||||
sha256sum $(DATA_FILES)
|
||||
|
||||
build:
|
||||
docker compose build
|
||||
|
||||
up:
|
||||
docker compose up -d
|
||||
|
||||
ps:
|
||||
docker compose ps
|
||||
|
||||
health:
|
||||
set -a; . ./.env; curl --fail --silent --show-error --retry 10 --retry-delay 1 --retry-connrefused --retry-all-errors -u "records:$${BASIC_AUTH_PASSWORD}" "http://127.0.0.1:$${APP_PORT:-$(APP_PORT)}/api/health"; echo
|
||||
|
||||
deploy: check smoke
|
||||
$(MAKE) --no-print-directory data-hash
|
||||
$(MAKE) --no-print-directory build
|
||||
$(MAKE) --no-print-directory up
|
||||
sleep 2
|
||||
$(MAKE) --no-print-directory health
|
||||
$(MAKE) --no-print-directory data-hash
|
||||
$(MAKE) --no-print-directory ps
|
||||
|
||||
logs:
|
||||
docker compose logs --tail=120 xsk-records-web
|
||||
@@ -5,8 +5,11 @@
|
||||
## 目录
|
||||
|
||||
- `app/`:FastAPI 后端和内置前端页面。
|
||||
- `MAINTENANCE.md`:日常检查、部署、数据保护和回滚流程。
|
||||
- `Makefile`:常用维护命令入口。
|
||||
- `scripts/deploy_to_vps.py`:部署网页服务到 VPS。
|
||||
- `scripts/sync_to_vps.py`:同步正式数据文件到 VPS。
|
||||
- `scripts/smoke_test.js`:轻量前端行为烟测。
|
||||
- `scripts/install_launch_agent.py`:安装 Mac 开机常驻同步任务。
|
||||
- `launchd/com.xsk.records.sync.plist.template`:LaunchAgent 模板。
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
const fs = require("fs");
|
||||
const vm = require("vm");
|
||||
|
||||
const elements = new Map();
|
||||
|
||||
function element(id) {
|
||||
if (!elements.has(id)) {
|
||||
elements.set(id, {
|
||||
id,
|
||||
textContent: "",
|
||||
innerHTML: "",
|
||||
hidden: false,
|
||||
disabled: false,
|
||||
value: "",
|
||||
dataset: {},
|
||||
classList: { toggle() {} },
|
||||
setAttribute(name, value) {
|
||||
this[name] = value;
|
||||
},
|
||||
addEventListener() {},
|
||||
focus() {},
|
||||
});
|
||||
}
|
||||
return elements.get(id);
|
||||
}
|
||||
|
||||
const documentStub = {
|
||||
querySelector(selector) {
|
||||
return element(selector.replace(/^#/, ""));
|
||||
},
|
||||
querySelectorAll() {
|
||||
return [];
|
||||
},
|
||||
addEventListener() {},
|
||||
createElement(tag) {
|
||||
return element(`created-${tag}-${elements.size}`);
|
||||
},
|
||||
body: { appendChild() {} },
|
||||
execCommand() {
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
const context = {
|
||||
console,
|
||||
document: documentStub,
|
||||
navigator: {},
|
||||
fetch: async () => ({ ok: true, json: async () => ({ classnotes: { mtime: 0 } }) }),
|
||||
Date,
|
||||
JSON,
|
||||
Map,
|
||||
Number,
|
||||
String,
|
||||
};
|
||||
|
||||
function assertEqual(name, actual, expected) {
|
||||
if (actual !== expected) {
|
||||
throw new Error(`${name}: got ${actual}, expected ${expected}`);
|
||||
}
|
||||
}
|
||||
|
||||
function value(expression) {
|
||||
return vm.runInContext(expression, context);
|
||||
}
|
||||
|
||||
vm.createContext(context);
|
||||
vm.runInContext(fs.readFileSync("app/static/app.js", "utf8"), context);
|
||||
|
||||
vm.runInContext(
|
||||
`
|
||||
currentRecords = [
|
||||
{ date: "2026.06.12", weekday: "星期五", time: "10:00-11:00", student: "甲", duration: "1小时0分", duration_hours: 1, teacher: "王老师", subject: "英语", _recordIndex: 0, _recordKey: "a" },
|
||||
{ date: "2026.06.12", weekday: "星期五", time: "08:00-09:00", student: "乙", duration: "1小时0分", duration_hours: 1, teacher: "王老师", subject: "英语", _recordIndex: 1, _recordKey: "b" },
|
||||
{ date: "2026.06.13", weekday: "星期六", time: "09:00-10:00", student: "丙", duration: "1小时0分", duration_hours: 1, teacher: "王老师", subject: "英语", _recordIndex: 2, _recordKey: "c" },
|
||||
{ date: "2026.06.12", weekday: "星期五", time: "07:00-08:00", student: "丁", duration: "1小时0分", duration_hours: 1, teacher: "李老师", subject: "数学", _recordIndex: 3, _recordKey: "d" },
|
||||
];
|
||||
renderGroupedRecords(currentRecords);
|
||||
`,
|
||||
context,
|
||||
);
|
||||
|
||||
assertEqual("初始日期按钮文案", value("dateSortBtn.textContent"), "日期 升序排列");
|
||||
assertEqual("初始时间按钮文案", value("timeSortBtn.textContent"), "时间 升序排列");
|
||||
assertEqual("默认组内排序", value("currentRecordOrder.join('')"), "bacd");
|
||||
|
||||
vm.runInContext("toggleRecordSort('date')", context);
|
||||
assertEqual("日期降序按钮文案", value("dateSortBtn.textContent"), "日期 降序排列");
|
||||
assertEqual("日期降序组内排序", value("currentRecordOrder.join('')"), "cbad");
|
||||
|
||||
vm.runInContext("toggleRecordSort('date'); toggleRecordSort('time')", context);
|
||||
assertEqual("时间降序按钮文案", value("timeSortBtn.textContent"), "时间 降序排列");
|
||||
assertEqual("日期升序时间降序组内排序", value("currentRecordOrder.join('')"), "abcd");
|
||||
|
||||
vm.runInContext(
|
||||
`
|
||||
correctedRecords.set("a", { ...currentRecords[0], date: "2026.06.11", weekday: "星期四", time: "07:00-08:00" });
|
||||
renderGroupedRecords(currentRecords);
|
||||
`,
|
||||
context,
|
||||
);
|
||||
assertEqual("纠错后排序仍使用显示记录", value("currentRecordOrder.join('')"), "abcd");
|
||||
|
||||
console.log("smoke test passed");
|
||||
Reference in New Issue
Block a user