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 后端和内置前端页面。
|
- `app/`:FastAPI 后端和内置前端页面。
|
||||||
|
- `MAINTENANCE.md`:日常检查、部署、数据保护和回滚流程。
|
||||||
|
- `Makefile`:常用维护命令入口。
|
||||||
- `scripts/deploy_to_vps.py`:部署网页服务到 VPS。
|
- `scripts/deploy_to_vps.py`:部署网页服务到 VPS。
|
||||||
- `scripts/sync_to_vps.py`:同步正式数据文件到 VPS。
|
- `scripts/sync_to_vps.py`:同步正式数据文件到 VPS。
|
||||||
|
- `scripts/smoke_test.js`:轻量前端行为烟测。
|
||||||
- `scripts/install_launch_agent.py`:安装 Mac 开机常驻同步任务。
|
- `scripts/install_launch_agent.py`:安装 Mac 开机常驻同步任务。
|
||||||
- `launchd/com.xsk.records.sync.plist.template`:LaunchAgent 模板。
|
- `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