chore: add reference docs, scripts, tests, demo, prototypes

- Reference docs: known issues, user journey, location redesign, mission merge plan
- Scripts: fill_location_state, merge_missions, migrate_location, migrate_state
- Tests: e2e test suite, asset entries unit tests
- Demo: location board, hifi prototypes, test screenshots
- Data backup: asset log entries and state nodes
- Update .gitignore

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 15:29:47 +08:00
co-authored by Claude Opus 4.7
parent f4909765af
commit e1d601a774
190 changed files with 11464 additions and 0 deletions
+368
View File
@@ -0,0 +1,368 @@
# 任务资产与日志 CRUD 复测报告
日期:2026-05-31
## 结论
本轮只做测试和报告,没有修改业务代码。
资产本体的新增、修改、删除主流程基本可用:测试资产可以创建,可以通过表单路由修改 `name/program/home_region/note`,也可以删除,删除后数据库中不再存在该测试资产。
资产日志和 state node 仍有 P0 问题。正式日志表单创建/编辑 state interval 时,entry 本体字段能保存,但 state node 的 `state_label``previous_location``transit_location``target_location` 全部没有保存。资产详情页的快速新增日志 modal 走 JSON 路由,这条路径更不完整:state/location/node location 字段被忽略;快速新增 Event Point 还会因为没有 start time 直接 400。
## 测试范围
按用户要求覆盖:
- 新建资产
- 修改资产信息
- 删除资产
- 新建资产日志
- 修改资产日志
- 修改 state node 信息,重点包括 target location
测试数据使用 `TEST_Codex_...` 前缀,测试结束后已清理。复测后查询确认剩余 `TEST_Codex_%` 资产数量为 0。
## 执行记录
### 1. 新建资产
通过 `/assets/new?sim_time=2060-03-12T09:00` UI 表单创建:
- Name: `TEST_Codex_Asset_20260531_719359`
- Asset Type: `Station`
- Program: `TEST Program Alpha`
- Home Region: `LEO`
- Note: `Created by Codex asset CRUD retest; safe to delete.`
结果:
- POST 后 302 跳转到资产详情页。
- 详情页 H1 显示测试资产名。
- DB 中存在该资产。
状态:通过。
### 2. 修改资产信息
通过 `/assets/<id>/edit` 表单路由提交:
- Name 改为 `TEST_Codex_Asset_20260531_719359_EDITED`
- Program 改为 `TEST Program Beta`
- Home Region 改为 `Mars Orbit`
- Note 改为 `Edited by Codex asset CRUD retest.`
结果:
```json
{
"status": 302,
"name": "TEST_Codex_Asset_20260531_719359_EDITED",
"program": "TEST Program Beta",
"home_region": "Mars Orbit",
"note": "Edited by Codex asset CRUD retest."
}
```
状态:通过。
备注:使用 in-app browser 直接填 edit form 时遇到浏览器控制层 CDP/clipboard 超时,我没有把它记为应用 bug;最终用同一个 HTTP 表单路由验证保存契约。
### 3. 删除资产
通过 `/assets/<id>/delete` 表单路由删除测试资产。
结果:
```json
{
"status": 302,
"location": "/assets?sim_time=2060-03-12T09:00",
"exists_after_delete": false
}
```
状态:通过。
## 日志与 State Node 问题
### P0-1: 正式日志表单没有保存 state node 的 location/state 字段
操作:
通过 `/assets/<id>/entries/new` 的正式表单路由创建 state interval
- Entry title: `TEST Formal State 20260531_719359`
- Mission: `TEST Mission Beta`
- Start/End: `2060-03-13T09:00``2060-03-16T09:00`
- Node title: `Formal Node A`
- Node state: `Docked`
- Previous Location: `LEO`
- Transit Location: `Transfer`
- Target Location: `Mars Orbit`
结果:
```json
{
"title": "Formal Node A",
"detail": "Formal node A detail",
"state_label": null,
"previous_location": null,
"transit_location": null,
"target_location": null
}
```
再通过 `/assets/<id>/entries/<entry_id>/edit` 修改同一个 node
- Node title 改为 `Formal Node A EDITED`
- Node state 改为 `Exploration`
- Previous Location 改为 `Mars Orbit`
- Transit Location 改为 `Transfer`
- Target Location 改为 `Mars Surface`
结果仍然是:
```json
{
"title": "Formal Node A EDITED",
"detail": "Formal node A detail edited",
"state_label": null,
"previous_location": null,
"transit_location": null,
"target_location": null
}
```
状态:未通过。
根因线索:
- [app/routes/web.py](<D:/My Coding Project/KSP_Project/app/routes/web.py:1080>) 中 `_parse_state_node_rows()``enumerate(..., start=1)`,但读取 `node_locations[index]` / `node_state_labels[index]`。表单数组是 0-based,第一个 node 应该用 `row_index`,当前第一个 node 永远读不到这些扩展字段。
- [app/routes/web.py](<D:/My Coding Project/KSP_Project/app/routes/web.py:1130>) 把 target location 存进 row key `"location"`
- [app/routes/web.py](<D:/My Coding Project/KSP_Project/app/routes/web.py:1157>) `_sync_state_nodes()` 却读取 `row.get("target_location")`。即使修掉 indextarget location 仍会因为 key 不一致继续丢失。
### P0-2: 资产详情页快速新增日志 JSON 路由忽略 location/state/node 扩展字段
资产详情页底部 quick modal 的 `saveLog()` 走 JSON
- `/assets/<id>/entries/new`
- `Content-Type: application/json`
这条后端路径只写:
- `entry_kind`
- `title`
- `summary`
- `start_at`
- `end_at`
- state node 的 `title/detail/at_time`
它没有写入:
- entry 的 `mission_label`
- node 的 `state_label`
- node 的 `previous_location`
- node 的 `transit_location`
- node 的 `target_location`
复测 payload 中传了这些字段:
```json
{
"state_label": "Maintenance",
"location": "Lunar Orbit",
"state_nodes": [
{
"state_label": "Maintenance",
"previous_location": "LEO",
"transit_location": "Transfer",
"target_location": "Lunar Orbit"
}
]
}
```
保存后 DB 中 node 仍是:
```json
{
"state_label": null,
"previous_location": null,
"transit_location": null,
"target_location": null
}
```
状态:未通过。
根因线索:
- [app/routes/web.py](<D:/My Coding Project/KSP_Project/app/routes/web.py:3917>) JSON create path 手写字段映射,没有复用 `_apply_asset_entry_fields()` / `_parse_state_node_rows()`
- [app/routes/web.py](<D:/My Coding Project/KSP_Project/app/routes/web.py:3989>) JSON edit path 也有同样问题。
- [app/templates/asset_detail.html](<D:/My Coding Project/KSP_Project/app/templates/asset_detail.html:252>) quick modal 的 `state_nodes` payload 只包含 `title/at_time/detail`UI 本身也没有 target location/state node fields。
### P0-3: 资产详情页快速新增 Event Point 无法保存
操作:
模拟资产详情页 `openNewLog('event')``saveLog()` 的 JSON 行为:`start_at=null``end_at=null`
结果:
- HTTP 400
- 数据库报错:`null value in column "start_at" of relation "asset_log_entries" violates not-null constraint`
原因:
- [app/templates/asset_detail.html](<D:/My Coding Project/KSP_Project/app/templates/asset_detail.html:229>) `openNewLog('event')` 会隐藏 `log-end-wrap`
- `log-end-wrap` 里同时包含 Start At 和 End At。
- 用户新建 Event Point 时没有地方填写 Start At。
- [app/templates/asset_detail.html](<D:/My Coding Project/KSP_Project/app/templates/asset_detail.html:261>) `saveLog()` 仍把 `start_at` 作为可空值发给后端。
状态:未通过。
### P1-1: 正式日志编辑页切换 Entry Type 的 JS 有潜在错误
代码中:
```js
document.getElementById("state-wrap").style.display = isState ? "" : "none";
```
但当前模板没有 `id="state-wrap"` 的元素。切换 entry type 时会触发空引用错误。
位置:
- [app/templates/asset_entry_form.html](<D:/My Coding Project/KSP_Project/app/templates/asset_entry_form.html:320>)
状态:未完整浏览器复现,但静态检查明确有风险。
### P1-2: 资产详情页日志搜索和类型筛选控件没有实际逻辑
资产详情页有:
- `#ad-lq`
- `#ad-lf`
但模板中没有对应筛选脚本,也没有表单提交参数。控件看起来可用,实际不会过滤日志表。
位置:
- [app/templates/asset_detail.html](<D:/My Coding Project/KSP_Project/app/templates/asset_detail.html:74>)
状态:静态检查明确未实现。
## 当前通过项
- 新建资产:通过。
- 修改资产本体字段:通过。
- 删除资产:通过,关联日志随资产清理。
- 正式日志表单的 entry 本体字段保存:部分通过,`title/mission_label/start_at/end_at/summary/note` 可保存和修改。
## 当前未通过项
- 正式日志表单保存 state node 的 `target_location`:失败。
- 正式日志表单保存 state node 的 `state_label/previous_location/transit_location`:失败。
- 资产详情页 quick modal 保存 state node 扩展字段:失败。
- 资产详情页 quick modal 新增 Event Point:失败。
- 资产详情页日志搜索/类型筛选:未实现。
- 正式日志编辑页切换 Entry Type:存在 JS 空引用风险。
## 改进建议
### P0
1. 统一日志保存路径。
- 不要让 JSON create/edit 和 form create/edit 各写一套字段映射。
- 建议把 request JSON 转成同一份 internal DTO,然后复用同一个 validation + persistence 函数。
2. 修复 `_parse_state_node_rows()` 的数组下标。
- 读取 node 扩展字段时使用 `row_index`,不要用从 1 开始的 `index`
3. 修复 target location key。
- `_parse_state_node_rows()` 应输出 `"target_location"`,或 `_sync_state_nodes()` 应读取 `"location"`;两边必须一致。
4. 资产详情 quick modal 要么删除,要么补齐。
- 如果保留 quick modalstate log 必须能填写 node state/previous/transit/target。
- Event Point 必须显示 Start At。
- JSON route 必须保存同等字段。
5. 后端 JSON 错误不要向用户暴露 SQLAlchemy/psycopg 全量错误。
- 当前 Event Point 失败时返回了完整 SQL、参数和 DB 约束信息。
- 应返回用户级 validation error,例如 `Start At is required`
### P1
1. 修复 `state-wrap` 空引用。
2. 实现资产详情日志搜索/类型筛选,或移除未接线控件。
3. 资产详情日志表应展示 node 的 state/location 字段,否则保存成功与否很难肉眼验证。
4. 给 location/state combobox 增加可测试的稳定选择器或 data attributes。
## 端到端测试方向
建议新增一个独立的资产 CRUD E2E 套件,必须使用唯一测试前缀并自动清理。
### Fixture
- 创建 `TEST_E2E_ASSET_<timestamp>`
- 固定 `sim_time=2060-03-12T09:00`
- 每个测试结束查询并删除 `TEST_E2E_%` 资产。
### 用例 1: 资产本体 CRUD
1. 打开 `/assets/new?sim_time=2060-03-12T09:00`
2. 填写 name/type/program/home_region/note。
3. 提交后断言跳到详情页。
4. 打开 edit 页修改 name/program/home_region/note/retired。
5. 提交后断言详情页 Quick Info 和 DB 一致。
6. 删除资产。
7. 断言列表页搜不到,DB 不存在。
### 用例 2: 正式 State Interval + State Node
1. 从资产详情点击 `+Entry` 或打开 `/assets/<id>/entries/new?kind=state`
2. 填写 entry title、mission、start/end、summary。
3. 添加两个 state nodes。
4. 每个 node 填 title/time/detail/state/previous/transit/target。
5. 保存后断言 DB 中两个 node 全字段一致,并按 `at_time` 排序。
6. 打开 edit 页修改第一个 node 的 target location。
7. 保存后断言 DB 更新。
8. 详情页断言 current location / log row location 使用最新 target location。
### 用例 3: Quick Modal State Log
1. 在资产详情点击 `+ State Interval`
2. 验证 modal 是否有足够字段保存 state node location/state。
3. 保存后断言 JSON route 与正式 form route 行为一致。
### 用例 4: Quick Modal Event Point
1. 在资产详情点击 `+ Event Point`
2. 断言 Start At 可见且必填。
3. 不填 Start At 时应显示 validation,不应返回原始 SQL 错误。
4. 填 Start At 后保存成功。
### 用例 5: 日志编辑和删除
1. 打开 entry edit 页。
2. 修改 entry 本体字段。
3. 修改已有 node。
4. 新增一个 node。
5. 删除一个 node。
6. 保存后断言 DB 与页面都一致。
7. 删除 entry 后断言关联 state nodes 级联删除。
### 用例 6: 资产详情日志筛选
1. 给同一 asset 创建 state 和 event 两类日志。
2. 输入搜索词,断言行数变化。
3. 选择 `State Interval`,断言只显示 state。
4. 选择 `Event Point`,断言只显示 event。
## 复测清理状态
- `TEST_Codex_Asset_20260531_719359` / edited variant:已删除。
- `TEST_Codex_EventModal_20260531_719359`:已删除。
- DB 查询 `TEST_Codex_%`0 条。