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 条。
+147
View File
@@ -0,0 +1,147 @@
# KSP管理后台 前端迁移测试方案
## 测试范围
基于 Phase 1-6 完成的所有前端迁移内容,共 12 个测试模块。
## 测试环境
- Flask 开发服务器 (port 9000)
- Playwright headless Chromium
- 数据库: PostgreSQL (真实数据)
---
## 模块 1: 基础框架
| # | 测试项 | 操作 | 预期结果 |
|---|--------|------|----------|
| 1.1 | 侧边栏导航 | 打开首页,检查侧边栏所有链接 | 侧边栏显示:任务总览、任务运营(4子项)、数据中心(4子项)、燃料换算、API健康检查 |
| 1.2 | 侧边栏展开 | 首页加载时检查 | 任务运营和数据中心分组默认展开 |
| 1.3 | 当前页高亮 | 导航到每个页面 | 对应侧边栏项显示 act 样式 |
| 1.4 | 深色模式 | 打开首页,检查默认主题 | 默认深色 (data-theme="dark") |
| 1.5 | 浅色模式切换 | 点击 ◐ 按钮 | 切换到浅色,localStorage 记忆 |
| 1.6 | 深色模式切换 | 再次点击 ◐ 按钮 | 切换回深色 |
| 1.7 | 模拟时间显示 | 检查顶栏 | 显示当前 sim_time |
| 1.8 | 时间 +1d | 点击 +1d 按钮 | 页面刷新,时间增加1天 |
| 1.9 | 时间 -1d | 点击 -1d 按钮 | 页面刷新,时间减少1天 |
| 1.10 | 时间 +7d | 点击 +7d 按钮 | 页面刷新,时间增加7天 |
| 1.11 | 时间 Today | 点击 Today 按钮 | 时间重置为 2060-03-12 |
| 1.12 | 时间 Modal | 点击时间显示 | 弹出时间编辑 modal |
| 1.13 | 时间 Modal Apply | 修改时间后点击 Apply | 页面刷新,时间更新 |
| 1.14 | 时间 Modal Cancel | 点击 Cancel | modal 关闭,时间不变 |
| 1.15 | ESC 关闭 Modal | 打开 modal 后按 ESC | modal 关闭 |
## 模块 2: 任务总览
| # | 测试项 | 操作 | 预期结果 |
|---|--------|------|----------|
| 2.1 | 页面标题 | 检查首页 | 显示 "任务总览" |
| 2.2 | 指标卡片 | 检查 metrics | 显示 Tracked Assets / Active Missions / Upcoming |
| 2.3 | 指标真实数据 | 检查指标数字 | 数字来自数据库,非硬编码 |
| 2.4 | 状态看板分组 | 检查 status board | 显示 In Transit / Exploration / Maintenance / Docked 分组 |
| 2.5 | 状态看板数据 | 检查各分组资产数 | 数字与数据库一致 |
| 2.6 | 资产链接 | 点击状态看板中的资产名 | 跳转到资产详情页 |
| 2.7 | 模拟时间影响 | 修改 sim_time 后检查 | 指标和状态看板按新时间重新计算 |
## 模块 3: 资产列表
| # | 测试项 | 操作 | 预期结果 |
|---|--------|------|----------|
| 3.1 | 页面渲染 | 导航到 /assets | 显示资产表格 |
| 3.2 | 搜索 | 输入搜索词 | 过滤匹配的资产 |
| 3.3 | 类型筛选 | 选择资产类型下拉 | 只显示该类型的资产 |
| 3.4 | 排序 | 选择排序选项 | 按选择排序 |
| 3.5 | 资产详情链接 | 点击资产名 | 跳转到 /assets/<id>/detail |
| 3.6 | New Asset 按钮 | 点击 + New Asset | 跳转到创建页面 |
| 3.7 | 分页 | 点击分页链接 | 正确翻页 |
## 模块 4: 资产详情
| # | 测试项 | 操作 | 预期结果 |
|---|--------|------|----------|
| 4.1 | 页面渲染 | 进入资产详情 | 显示名称、类型、状态 |
| 4.2 | 信息卡片 | 检查4张卡片 | 显示 Type / Location / Mission / Entries |
| 4.3 | Docking Target | 检查对接目标区域 | 显示 "Not docked" 或已对接资产 |
| 4.4 | Docked Vehicles | 检对接载具列表 | 显示已对接载具或 "No vehicles" |
| 4.5 | 日志列表 | 检查日志表格 | 显示日期、类型、标题、状态、位置、操作 |
| 4.6 | 日志搜索 | 输入搜索词 | 过滤日志 |
| 4.7 | 日志类型筛选 | 选择 State/Event | 按类型过滤 |
| 4.8 | Edit Asset 按钮 | 点击 Edit Asset | 跳转编辑页 |
| 4.9 | ← Back 按钮 | 点击返回 | 回到资产列表 |
## 模块 5: 对接功能
| # | 测试项 | 操作 | 预期结果 |
|---|--------|------|----------|
| 5.1 | + Dock 按钮 | 点击 + Dock | 弹出对接 modal |
| 5.2 | 对接 modal 内容 | 检查 modal | 显示资产下拉 + Custom name 选项 + 备注 + Cancel/Dock |
| 5.3 | 选择已有资产对接 | 选择一个资产 + Dock | 刷新页面,显示对接记录 |
| 5.4 | Custom name 对接 | 选 Custom name + 输入名称 + Dock | 刷新页面,显示对接记录 |
| 5.5 | 对接后日志 | 检查日志列表 | 新增一条 dock 相关的日志条目 |
| 5.6 | Cancel 对接 | 点击 Cancel | modal 关闭,无变化 |
## 模块 6: 日志 CRUD
| # | 测试项 | 操作 | 预期结果 |
|---|--------|------|----------|
| 6.1 | + State Interval | 点击按钮 | 弹出 modal,显示 Start/End/State/Location/Summary/Nodes |
| 6.2 | + Event Point | 点击按钮 | 弹出 modal,隐藏 End/State/Nodes |
| 6.3 | 新增 State Interval | 填写表单 + Save | 刷新页面,新日志出现在列表中 |
| 6.4 | 新增 Event Point | 填写表单 + Save | 刷新页面,新事件出现在列表中 |
| 6.5 | State Node 添加 | 点击 "+ Add Node" | 新增一个节点行 |
| 6.6 | State Node 删除 | 点击 ✕ | 节点行删除 |
| 6.7 | State Node 数据 | 填写多个节点 + Save | 日志保存后,每个节点正确存储 |
| 6.8 | 编辑日志 | 点击 Edit | 弹出 modal,预填现有数据 |
| 6.9 | 编辑后保存 | 修改标题 + Save | 刷新页面,标题已更新 |
| 6.10 | 删除日志 | 点击 Del | confirm 后日志删除,entries 计数减少 |
## 模块 7: 地点板
| # | 测试项 | 操作 | 预期结果 |
|---|--------|------|----------|
| 7.1 | 页面渲染 | 导航到地点板 | 显示 Canvas 星图 + 右侧信息面板 |
| 7.2 | 星图内容 | 检查 Canvas | 显示太阳系行星(太阳、地球、火星等) |
| 7.3 | 右侧面板 | 检查 info panel | 显示 Scope / Assets in Scope / Ongoing Missions |
| 7.4 | 资产位置 | 检查 Assets in Scope | 资产按 location 分组到对应天体 |
| 7.5 | 点击行星 | 点击木星 | 进入行星系视图,显示卫星 |
| 7.6 | 点击卫星 | 在木星系点击 Io | 进入 Io 单体视图 |
| 7.7 | 返回 | 点击 ← Back | 返回上一层 |
| 7.8 | 拖拽平移 | 在星图上拖拽 | 星图跟随移动 |
| 7.9 | 滚轮缩放 | 在星图上滚动 | 星图缩放 |
## 模块 8: 时间线
| # | 测试项 | 操作 | 预期结果 |
|---|--------|------|----------|
| 8.1 | 页面渲染 | 导航到时间线 | 显示时间线 + 筛选栏 |
| 8.2 | Scale 切换 | 选择 Year/Month/Day | 时间线刻度变化 |
| 8.3 | 搜索筛选 | 输入资产名 | 只显示匹配资产 |
| 8.4 | Type 筛选 | 选择类型 | 只显示该类型资产 |
| 8.5 | Location 筛选 | 选择位置 | 只显示该位置资产 |
| 8.6 | State 筛选 | 选择状态 | 只显示该状态资产 |
## 模块 9: 数据中心
| # | 测试项 | 操作 | 预期结果 |
|---|--------|------|----------|
| 9.1 | 引擎列表 | /engines | 显示引擎表格,有搜索/筛选 |
| 9.2 | 通信列表 | /communications | 显示通信部件表格 |
| 9.3 | 燃料箱列表 | /tanks | 显示燃料箱表格 |
| 9.4 | 载具列表 | /vehicles | 显示载具表格 |
| 9.5 | 搜索 | 在引擎列表输入搜索 | 过滤结果 |
| 9.6 | 筛选 | 选择燃料类型 | 过滤结果 |
## 模块 10: 燃料换算
| # | 测试项 | 操作 | 预期结果 |
|---|--------|------|----------|
| 10.1 | 页面渲染 | /fuel-converter | 显示换算器 |
| 10.2 | 换算 L→t | 输入值 + L→t + Convert | 显示所有燃料的结果 |
| 10.3 | 换算 t→L | 切换模式 + Convert | 显示所有燃料的结果 |
## 测试执行方式
- 使用 Playwright headless Chromium
- 每个模块独立测试函数
- 每个测试项输出 PASS/FAIL
- 失败时记录具体原因
- 最终输出汇总报告
+142
View File
@@ -0,0 +1,142 @@
# KSP 管理后台重构测试计划 v2
生成日期:2026-05-31
## 1. 项目与重构摘要
本项目是一个 Flask + SQLAlchemy 的 KSP 数据管理后台,数据来源包括引擎/通信/燃料箱/载具 Excel,以及任务资产日志。早期能力集中在数据中心:引擎目录、通信部件、燃料箱规格、载具成本、燃料换算和导入脚本。
`draft_board.md` 描述的重构重点是“任务运营”:
- 资产当前位置、当前任务、当前状态由 `AssetLogEntry``simulation_time` 派生,不应直接编辑位置结果。
- 资产列表、任务总览、状态板、任务列表、时间线和地点板都应围绕同一套资产日志快照计算。
- 日志支持 state interval / event point,并支持 state nodes;节点应按时间排序。
- 对接关系应是时间敏感模型,需求中规划了 `docking_events` 表,并要求已有资产对接时双向写入日志。
- 地点板应提供 Canvas 星图、天体层级导航、缩放/拖拽和 scope 面板。
- 时间线应提供只读时间轴、搜索、预设、类型/位置/状态/日期范围/资产选择等筛选。
- 数据中心保持现有结构,重构不应破坏 CRUD、筛选、排序和详情页。
当前实现与 `draft_board.md` 的明显差距:
- 模型和迁移中尚未实现 `docking_events` 表;当前 `/api/v1/assets/<id>/dock` 只给当前资产新增一条 state 日志,没有真实父子对接关系、分离流程或双向日志。
- Location 仍是日志自由文本字段,尚未落成结构化层级选项。
- 地点板已接入 Canvas 和轨道数据,但当前模板使用程序绘制的渐变天体,没有使用 `demo/textures/` 的真实纹理。
- 时间线模板渲染了筛选控件,但未发现 `tlPreset`、搜索/筛选控件事件处理脚本;这些控件需要被测试明确捕获。
- 资产详情日志搜索/类型筛选控件存在 DOM,但未发现对应前端过滤脚本。
## 2. 对现有 `docs/test_plan.md` 的评估
现有计划覆盖面大体方向正确,但不能直接作为验收依据:
- 文档称“12 个测试模块”,实际只写了 10 个模块。
- 大量断言只验证元素存在,例如按钮、表格、canvas,而没有验证数据来源、交互结果、URL 参数、数据库派生逻辑或副作用。
- 多个用例与当前实现不一致:日志编辑实际是跳转到编辑页,不是打开 modal;对接功能没有 `docking_events`;时间线筛选控件缺少脚本;地点板没有真实纹理切换。
- 测试脚本 `tests/test_e2e.py` 只有 36 个轻量断言,且结果名称编码损坏;它没有执行关键 CRUD 写入、分离、双向日志、时间敏感对接、筛选功能有效性,也没有隔离数据库。
- 测试环境写“真实 PostgreSQL”,但计划包含新增/删除/对接写入,这会污染生产式数据。写入类测试必须切换到隔离测试库或事务回滚。
## 3. 测试策略
测试分三层执行。
### A. 非破坏性冒烟回归
目标:确认服务可达、页面主结构存在、关键数据表可渲染、API 基本可用。
适用环境:当前真实 PostgreSQL 或只读数据环境。
执行方式:
- HTTP GET 页面检查。
- API GET/错误输入检查。
- 静态模板/脚本契约检查。
- 不新增、不编辑、不删除、不调用会持久写入的对接接口。
### B. 浏览器交互回归
目标:确认 JS 交互真的工作,而不是只存在 DOM。
适用环境:可运行 Playwright/Browser 的本地环境。
执行方式:
- 点击主题切换、时间 modal、侧边栏、地点板 canvas、时间线筛选控件。
- 检查 URL、DOM、canvas 像素和 console error。
- 只执行不写库的交互,除非已切到测试库。
### C. 写入与业务验收
目标:验证重构核心的业务正确性。
适用环境:隔离 PostgreSQL/SQLite 测试库,或每个测试事务回滚。
执行方式:
- 使用固定 fixture 创建资产、日志、节点、对接事件。
- 测试完成后清理或回滚。
- 严禁在真实数据环境直接执行新增/删除/对接/导入。
## 4. v2 测试矩阵
| 模块 | 优先级 | 用例 | 类型 | 预期 |
|---|---:|---|---|---|
| 服务/API | P0 | `/api/v1/health` | 冒烟 | HTTP 200`status=ok`service 名正确 |
| 服务/API | P0 | 燃料列表 API | 冒烟 | 返回 8 个燃料因子 |
| 服务/API | P1 | 燃料换算 API `mode=volume/mass` | 冒烟 | 返回每种燃料的换算结果 |
| 服务/API | P1 | 燃料换算非法输入 | 冒烟 | HTTP 400,错误信息明确 |
| 基础框架 | P0 | 首页可达 | 冒烟 | HTTP 200,显示任务总览 |
| 基础框架 | P0 | 侧边栏 | 冒烟 | 显示任务运营、数据中心、燃料换算、API 健康检查 |
| 基础框架 | P1 | 主题切换 | 浏览器 | `data-theme` 在 dark/light 间切换,localStorage 持久化 |
| 基础框架 | P1 | 模拟时间 modal | 浏览器 | 可打开/关闭/ESC 关闭,Apply 后 URL 写入 `sim_time` |
| 任务总览 | P0 | 指标与状态板渲染 | 冒烟 | Tracked Assets / Active Missions / Upcoming / Status Board 存在 |
| 任务总览 | P1 | 模拟时间影响 | 业务 | 同一 fixture 在不同 `sim_time` 下派生状态和 upcoming 变化正确 |
| 资产列表 | P0 | `/assets` 可达 | 冒烟 | 表格有数据行,显示当前位置/当前事件/日志数 |
| 资产列表 | P0 | 搜索路由 | 冒烟 | `q=ST-01` 返回匹配资产 |
| 资产列表 | P1 | 类型/排序 | 浏览器 | 下拉改变后请求参数正确,结果排序/过滤正确 |
| 资产列表 | P1 | 退役资产默认隐藏 | 业务 | 默认不显示 retired,显式开关后显示 |
| 资产详情 | P0 | 从列表进入详情 | 冒烟 | 显示信息卡、Docking Target、Docked Vehicles、Log Entries |
| 资产详情 | P1 | 当前状态/位置派生 | 业务 | state interval 覆盖当前时间时取 active state;无 active 时取最近日志或 home_region |
| 资产详情 | P1 | 日志分页 | 浏览器 | 多页日志翻页后保留 `sim_time` |
| 资产详情 | P2 | 日志搜索/类型筛选 | 浏览器 | 输入/选择后列表实际收缩;若控件无脚本应失败 |
| 日志 CRUD | P0 | 新增 state interval | 写入 | 写入后列表出现,state nodes 按 `at_time` 升序 |
| 日志 CRUD | P0 | 新增 event point | 写入 | event 无 end/state/nodes 字段要求,保存后可见 |
| 日志 CRUD | P0 | 编辑日志页 | 写入 | GET 预填原值,POST 修改后持久化,失败时保留输入并显示 validation dialog |
| 日志 CRUD | P1 | 删除日志 | 写入 | 删除后 entries 计数减少,关联 nodes 级联删除 |
| 对接功能 | P0 | 需求级数据模型 | 静态/迁移 | 存在 `docking_events` 表和模型字段;当前应失败 |
| 对接功能 | P0 | 已有资产对接 | 写入 | parent/child 关系可按时间查询,双方日志/节点按需求写入 |
| 对接功能 | P0 | custom name 对接 | 写入 | 只影响当前 asset,保留 child_label |
| 对接功能 | P0 | 分离 | 写入 | undocked_at 写入,当前时间晚于分离时不再显示 docked |
| 状态板 | P0 | `/mission-preview/status-board` 可达 | 冒烟 | 显示状态分组、筛选栏、upcoming |
| 状态板 | P1 | 筛选 | 浏览器 | 资产类型/地点/记录范围筛选后卡片数量和 URL 参数正确 |
| 任务列表 | P1 | `/missions` 可达 | 冒烟 | 显示任务/日志聚合列表 |
| 任务列表 | P1 | 筛选 | 浏览器 | 类型/地点/记录范围筛选正确 |
| 地点板 | P0 | `/mission-preview/location-board` 可达 | 冒烟 | canvas、Scope、Assets in Scope、Ongoing Missions 存在 |
| 地点板 | P0 | canvas 非空 | 浏览器 | 截图或像素采样证明非空,console 无错误 |
| 地点板 | P1 | 层级导航 | 浏览器 | 点击 Jupiter 进入 Jupiter System,点击 Io 进入 IoBack 返回 |
| 地点板 | P1 | 缩放/拖拽 | 浏览器 | wheel 改变缩放,拖拽改变视图偏移 |
| 地点板 | P1 | 资产 scope | 业务 | 不同 scope 下 Assets in Scope 与日志 location 派生一致 |
| 地点板 | P2 | 真实纹理 | 视觉/静态 | 使用真实纹理资源或明确降级;当前实现应标记未达 draft 目标 |
| 时间线 | P0 | `/mission-preview/timeline` 可达 | 冒烟 | 时间线行和刻度渲染 |
| 时间线 | P1 | scale 切换 | 浏览器 | Year/Month/Day 改变 URL 或刻度 |
| 时间线 | P1 | 搜索/类型/地点/状态筛选 | 浏览器 | 控件实际改变可见行;当前缺少脚本时应失败 |
| 时间线 | P1 | 日期范围 | 浏览器/业务 | 手动范围生效,segment 裁切百分比正确 |
| 数据中心 | P0 | 四个列表页 | 冒烟 | `/engines` `/communications` `/tanks` `/vehicles` 均 HTTP 200 且有数据 |
| 数据中心 | P1 | 筛选/排序 | 浏览器 | 查询参数生效,表格结果正确 |
| 数据中心 | P1 | CRUD | 写入 | 新增/编辑/删除后数据库一致,失败时 validation 明确 |
| 导入 | P1 | workbook inspect/import dry run | 业务 | 能识别表结构、自然键冲突、导入统计 |
| 可访问性/前端质量 | P1 | console error | 浏览器 | 主路径无 JS error |
| 可访问性/前端质量 | P2 | 响应式 | 视觉 | 1440x900、390x844 下无严重重叠 |
## 5. 自动化建议
- 保留一个 `smoke` 套件:只读、可在真实库跑,覆盖 P0 冒烟项。
- 新增一个 `e2e` 套件:Playwright + 测试库,覆盖 P1 浏览器交互。
- 新增一个 `business` 套件:Flask test client + fixture + 事务回滚,覆盖日志派生、对接、时间敏感状态。
- 测试报告必须输出 Markdown 与机器可读 JSON;失败项保留 URL、参数、截图或最小复现步骤。
- 所有写入测试统一使用唯一前缀,例如 `TEST_E2E_20260531_`,并在 teardown 校验清理完成。
## 6. 本轮建议的验收门槛
- P0 冒烟:100% 通过。
- P0 业务:对接模型完成前允许标记为“未实现”,但不能误报通过。
- P1 浏览器交互:主路径无 console error,地点板 canvas 非空,时间线筛选可实际生效。
- 写入测试:只允许在隔离库执行;真实库环境下报告为 skipped,而不是 pass。
+187
View File
@@ -0,0 +1,187 @@
{
"total": 36,
"passed": 36,
"failed": 0,
"results": [
{
"name": "1.1 侧边栏导航",
"ok": true,
"detail": "11 items"
},
{
"name": "1.2 侧边栏默认展开",
"ok": true,
"detail": "2 expanded"
},
{
"name": "1.3 当前页高亮",
"ok": true,
"detail": ""
},
{
"name": "1.4 默认深色模式",
"ok": true,
"detail": "theme=dark"
},
{
"name": "1.5 浅色模式",
"ok": true,
"detail": ""
},
{
"name": "1.6 深色模式切换回",
"ok": true,
"detail": ""
},
{
"name": "1.7 模拟时间显示",
"ok": true,
"detail": ""
},
{
"name": "1.12 Modal打开",
"ok": true,
"detail": ""
},
{
"name": "1.14 Cancel关闭",
"ok": true,
"detail": ""
},
{
"name": "1.15 ESC关闭",
"ok": true,
"detail": ""
},
{
"name": "2.1 标题",
"ok": true,
"detail": ""
},
{
"name": "2.2 指标卡片",
"ok": true,
"detail": "3 cards"
},
{
"name": "2.3 真实数据",
"ok": true,
"detail": "count=14"
},
{
"name": "2.4 状态看板",
"ok": true,
"detail": "4 groups"
},
{
"name": "2.6 资产链接",
"ok": true,
"detail": ""
},
{
"name": "3.1 列表渲染",
"ok": true,
"detail": "14 rows"
},
{
"name": "3.2 搜索",
"ok": true,
"detail": "14->1"
},
{
"name": "4.1 详情渲染",
"ok": true,
"detail": ""
},
{
"name": "4.2 信息卡片",
"ok": true,
"detail": "4 cards"
},
{
"name": "4.5 日志列表",
"ok": true,
"detail": ""
},
{
"name": "5.1 Dock按钮",
"ok": true,
"detail": ""
},
{
"name": "5.2 Modal内容",
"ok": true,
"detail": "15 options"
},
{
"name": "5.6 Cancel",
"ok": true,
"detail": ""
},
{
"name": "6.1 State Interval",
"ok": true,
"detail": ""
},
{
"name": "6.5 Node添加",
"ok": true,
"detail": ""
},
{
"name": "6.10 Modal关闭",
"ok": true,
"detail": ""
},
{
"name": "7.1 Canvas",
"ok": true,
"detail": ""
},
{
"name": "7.2 范围",
"ok": true,
"detail": ""
},
{
"name": "8.1 渲染",
"ok": true,
"detail": ""
},
{
"name": "8.2 数据",
"ok": true,
"detail": "14 rows"
},
{
"name": "9.engines",
"ok": true,
"detail": "30 rows"
},
{
"name": "9.comms",
"ok": true,
"detail": "30 rows"
},
{
"name": "9.tanks",
"ok": true,
"detail": "26 rows"
},
{
"name": "9.vehicles",
"ok": true,
"detail": "10 rows"
},
{
"name": "10.1 渲染",
"ok": true,
"detail": ""
},
{
"name": "JS Errors",
"ok": true,
"detail": "0 errors"
}
]
}
+77
View File
@@ -0,0 +1,77 @@
# KSP 管理后台测试报告
执行日期:2026-05-31
## 1. 执行结论
本轮完成了项目重构梳理、现有测试计划评估,并生成新的 `docs/test_plan_v2.md`
实际执行测试采用非破坏性 HTTP/API/模板级回归检查,目标是避免污染当前 `.env` 指向的真实 PostgreSQL 数据库。共执行 24 项,24 通过,0 失败。
浏览器自动化和现有 Python Playwright 脚本未能执行:本机 `.venv\Scripts\python.exe` 指向不存在的 Python 安装,`py -3` 不可用;Codex in-app Browser 的 Node 会话在本机 sandbox 中连续启动失败。因此,本报告不把 JS 点击、canvas 像素、CRUD 写入类场景算作已通过。
## 2. 测试环境
- 运行服务:`http://127.0.0.1:9000`
- 应用端口:`.env``APP_PORT=9000`
- 数据库:`.env` 指向 PostgreSQL `PGHOST=192.168.195.241``PGDATABASE=KSP`
- 测试方式:PowerShell `Invoke-WebRequest` / `Invoke-RestMethod`
- 数据安全策略:不执行新增、编辑、删除、导入、对接保存等持久写入动作
## 3. 已执行测试
| # | 检查项 | 结果 | 详情 |
|---:|---|---|---|
| 1 | API health returns ok | PASS | `{"service":"ksp-data-hangar","status":"ok"}` |
| 2 | Dashboard reachable | PASS | HTTP 200 |
| 3 | Base shell/sidebar present | PASS | sidebar、任务运营、数据中心、燃料换算存在 |
| 4 | Global sim time control present | PASS | `sim-time-display``+1d`、Today 存在 |
| 5 | Dashboard data panels present | PASS | Tracked Assets / Active Missions / Upcoming / Status Board 存在 |
| 6 | Asset list reachable with rows | PASS | rows=15 |
| 7 | Asset list filters/actions present | PASS | `q``asset_type``sort`、New Asset 存在 |
| 8 | Asset search route returns filtered page | PASS | `/assets?q=ST-01` 返回匹配页面 |
| 9 | Asset detail reachable from list link | PASS | `/assets/fba95050-ca89-4e67-b796-66b7e1c40764?...` |
| 10 | Asset detail dock/log modals present | PASS | `dock-modal``log-modal``saveDock``saveLog` 存在 |
| 11 | Mission status board reachable | PASS | HTTP 200 |
| 12 | Mission status board filters present | PASS | `asset_type``location``record_scope` 存在 |
| 13 | Location board reachable | PASS | HTTP 200`map-cv``LOC_ASSETS` 存在 |
| 14 | Location board embeds orbital data | PASS | Keplerian/ORB/BODIES 数据存在 |
| 15 | Timeline reachable | PASS | HTTP 200`tlr` 存在 |
| 16 | Timeline filters present | PASS | `tl-q``tls``tl-type``tl-loc``tl-state` 存在 |
| 17 | Data center: Engines | PASS | rows=31 |
| 18 | Data center: Communications | PASS | rows=31 |
| 19 | Data center: Tanks | PASS | rows=27 |
| 20 | Data center: Vehicles | PASS | rows=11 |
| 21 | Fuel converter page reachable | PASS | HTTP 200 |
| 22 | Fuel factors API returns list | PASS | count=8 |
| 23 | Fuel convert API volume mode | PASS | mode=volumecount=8 |
| 24 | Fuel convert invalid input rejected | PASS | HTTP 400 |
## 4. 未执行/跳过项
| 项目 | 状态 | 原因 |
|---|---|---|
| `tests/test_e2e.py` | SKIPPED | 本地 Python 不可用:`.venv` 入口坏,`py -3` 未安装 |
| Playwright 浏览器点击测试 | SKIPPED | 浏览器自动化运行时启动失败 |
| 主题切换、时间 modal、ESC、点击导航 | SKIPPED | 需要浏览器 JS 执行环境 |
| 地点板 canvas 像素/点击 Jupiter/Io/Back/缩放拖拽 | SKIPPED | 需要浏览器和截图/像素采样 |
| 时间线筛选实际生效 | SKIPPED | 需要浏览器 JS 执行环境 |
| 日志 CRUD、对接保存、删除、导入 | SKIPPED | 当前连接真实 PostgreSQL,执行会污染数据 |
## 5. 发现的问题
1. 现有 `docs/test_plan.md` 与实际不一致:文档写“12 个模块”,实际只有 10 个模块。
2. 现有 `tests/test_e2e.py` 的断言过浅,且报告名称编码损坏;它更像 smoke test,不能证明重构验收通过。
3. `draft_board.md` 要求的 `docking_events` 表尚未落地;当前对接接口只是给当前资产新增 state 日志,没有双向关系、分离或时间敏感查询。
4. 时间线模板存在 `onclick="tlPreset(...)"`,但代码库中未找到 `tlPreset` 实现;搜索/类型/地点/状态筛选控件也未找到绑定逻辑。
5. 资产详情日志搜索 `ad-lq` 和类型筛选 `ad-lf` 存在 DOM,但未找到前端过滤脚本。
6. 地点板当前用 Canvas 渐变绘制天体;尚未实现 draft 中“真实纹理 + 渲染模式切换”的目标。
7. 燃料换算页面是前端本地计算,API 另有 `/api/v1/fuel-converter/convert`;两者的模式命名不同,后续测试需同时覆盖页面行为和 API 合约。
## 6. 建议
- 先修复本地 Python/Playwright 环境,恢复自动化测试可执行性。
- 将当前 `tests/test_e2e.py` 拆成 `smoke``browser``business` 三类,避免把 DOM 存在误当成功能通过。
- 为写入测试准备隔离数据库或事务回滚 fixture,再验证日志 CRUD、对接和分离。
- 对时间线和资产详情筛选先补前端逻辑,再纳入 P1 浏览器回归。
-`docking_events` 明确是“本期必须实现”还是“后续 backlog”;否则测试计划应将其标为未实现验收项。
@@ -0,0 +1,272 @@
# 时间线页面复测报告
日期:2026-05-31
## 结论
其他团队的修改有部分推进,但时间线页面还不能算完成。已改善的点包括:`scale=day` 时下拉框能正确显示 Day、segment 外层宽度开始使用 `track_width_pct`、asset 名称已具备链接、`_partial=1` 从 HTML 改成了 JSON,单次响应体从上一轮约 315 KB 降到当前约 128 KB。
但复测发现新的关键回归:页面加载后客户端 JS 会直接替换 `#tl-canvas.innerHTML`,把时间刻度行清掉;滚轮/按钮缩放没有可靠生效;Auto scale 在手动范围下仍然错误;事件点仍然只是无文字小点;资产筛选仍不是需求里的“type/location 筛选后资产多选默认全选”。
## 测试环境
- 本地服务:`http://127.0.0.1:9000`
- 页面:`/mission-preview/timeline`
- 自动化:Codex in-app Browser + 现有 `tests/test_e2e.py`
- 当前工作区包含大量未提交改动,本报告只做复测与记录,不修改业务代码。
## 执行的检查
### HTTP / 路由检查
1. `GET /mission-preview/timeline`
- HTTP 200
- 响应体约 512 KB
- 包含 `tl-viewport` / `tl-canvas`
2. `GET /mission-preview/timeline?_partial=1`
- HTTP 200
- `Content-Type: application/json`
- 响应体约 128 KB
- `scale=year`
- `bucket_count=15`
- `rows=14`
3. `GET /mission-preview/timeline?scale=auto&start=2057-04-24&end=2057-07-21&range_mode=manual&_partial=1`
- HTTP 200
- `range_start=2057-04-24T00:00`
- `range_end=2057-07-21T00:00`
- 实际 `scale=year`
- 实际 `bucket_count=1`
- 这是错误结果。88 天范围应该自动进入 Day 或至少 Month。
4. `GET /mission-preview/timeline?scale=day&start=2057-04-24&end=2057-07-21&range_mode=manual&_partial=1`
- HTTP 200
- `scale=day`
- `bucket_count=89`
- 说明显式 Day 路径可工作。
### 浏览器 DOM 检查
初始打开 `/mission-preview/timeline` 后,等待 JS 执行完成:
- `#tl-canvas.children.length = 14`
- DOM 中 asset 行数 = 14
- segment 数 = 116
- event 数 = 238
- `timeline_json.scale = year`
- `timeline_json.bucket_count = 15`
- `select[name=scale].value = year`
- `window.tlZoom = undefined`
- `window.tlResetView = undefined`
重要现象:客户端渲染后,`#tl-canvas` 的第一个子元素已经是 `Europa Outpost` 的资产行,不再是时间刻度行。也就是说,服务端首屏渲染出来的刻度行被 JS 清掉了。
### 现有 E2E
执行命令:
```powershell
python tests\test_e2e.py
```
结果:
- 总计 36 项
- 通过 34 项
- 失败 2 项
- 失败项均为时间线:
- `8.1 渲染`
- `8.2 数据 -- 0 rows`
说明:现有 E2E 仍在找旧 DOM 选择器 `#tls` / `.tl-r`,已经与当前实现失配。这个失败不等价于页面完全不可用,但说明测试套件没有跟随时间线重构更新,无法作为时间线验收依据。
## 逐项复测结果
### 1. 滚轮缩放卡顿
状态:部分改善,但未通过。
改善点:
- `_partial=1` 改成 JSON 后,单次 partial 数据量下降到约 128 KB。
- 新实现意图是在客户端用 `timeline_json` 直接重绘,理论上可以避免上一版每次 wheel 都请求后端。
仍存在的问题:
- 实测滚轮操作没有改变 hidden `start/end`,也没有改变 segment 位置。
- `Zoom +` / `Zoom -` / `Reset View` 按钮仍然存在,但模板里没有定义 `tlZoom()``tlResetView()`
- 客户端 zoom 逻辑只调整 `bucket_count`,没有按新的 `range_start/range_end` 重新计算每个 segment/event 相对位置。即使 wheel 触发成功,也会出现“缩放但不按新窗口重定位”的问题。
证据:
- `window.tlZoom``undefined`
- `window.tlResetView``undefined`
- 点击按钮对应的 inline handler 是 `tlZoom(1.3)` / `tlResetView()`
### 2. 滚轮缩放丢失日志/资产
状态:旧丢参问题缓解,但新实现仍有风险。
已改善:
- 客户端缩放不再发 AJAX,因此上一版 wheel AJAX 丢 `asset_ids` / `record_scope` 的问题不再以同样形式出现。
- URL 手动传 2 个 `asset_ids` 时,DOM 确实只渲染这 2 个 asset。
仍存在的问题:
- 默认资产多选框没有任何选中项,但页面渲染 14 个 asset。UI 表达仍然是“未选择”,实际语义却是“全部显示”。
- 客户端缩放数据来自初始 `timeline_json`,如果后续改为 partial 更新,需要重新验证所有 filter 参数是否同步。
证据:
- 默认打开时:asset options = 14selected count = 0timeline rows = 14。
-`asset_ids=A&B` 打开时:selected count = 2timeline rows = 2。
### 3. 缩放时自动切换年/月/日
状态:未通过。
显式 `scale=day` 已经可以显示 Day,并且 JSON 返回 `bucket_count=89`。但 `scale=auto` 或不传 `scale` 且传入 88 天手动范围时,后端仍返回 `scale=year``bucket_count=1`
根因判断:
- `mission_timeline_preview()` 在解析 `range_start/range_end` 之前先调用 `_resolve_timeline_scale(request.args.get("scale"), initial_range_start, initial_range_end)`
- 这导致 auto scale 使用的是默认全量时间范围,而不是用户当前手动范围。
- 此外 `<option value="auto">` 永远不会保持 Auto selected,因为后端最后传给模板的是解析后的 `year/month/day`,不是原始 scale mode。
### 4. Type/location 筛选后资产多选默认全选
状态:未通过。
当前行为:
- Type 和 Location 仍是普通单选下拉。
- Assets 仍是原生 `<select multiple>`
- 默认 selected count = 0,但页面显示全部资产。
- 没有 checkbox dropdown、Select All、Clear、已选数量展示。
- 没有 State 筛选。
需求目标是“先根据 type 和 location 进行筛选,筛选结果进入下拉多选框,默认全选,仅展示选中的 asset”。当前仍是“空 asset_ids 表示全部”,没有做到视觉和语义一致。
### 5. Event point 显示效果
状态:未通过。
当前 DOM
- `.tl-evt` 数量 = 238
- 有可见文本的 `.tl-evt` 数量 = 0
- connector 数量 = 0
- state node 与普通 event 没有稳定的 class/data 区分
后端仍然计算了事件 label 宽度、label side、track、connector height,但当前客户端渲染没有使用这些字段。页面仍然只是 7px 小圆点,不能扫描事件内容。
### 6. 点击 asset 跳转到 asset 页面
状态:基本通过,但还需要人工点击验收。
当前 asset label 已渲染为链接:
```text
/assets/fba95050-ca89-4e67-b796-66b7e1c40764?sim_time=2060-08-11T10:00
```
这比上一版 plain div 有明显改善。需要注意的是当前 `simulation_time` 来自 DB/session,复测时已变成 `2060-08-11T10:00`,不是原本默认的 `2060-03-12T09:00`。这不是时间线链接本身的问题,但全局 simulation time 被写 DB 后会影响所有页面和测试基线,建议测试报告里固定 sim_time。
## 新发现的问题
### A. 时间刻度行被客户端渲染清除
模板服务端先渲染了刻度行:
```html
<div id="tl-canvas">
<div>scale labels...</div>
...
</div>
```
但脚本执行:
```js
if (tlCanvas) tlCanvas.innerHTML = buildHTML(tlData);
```
`buildHTML()` 只生成 asset rows,不生成 scale labels。因此用户最终看到的 DOM 没有时间刻度行。
### B. 缩放按钮是坏控件
页面底部仍显示:
- `Zoom +`
- `Zoom -`
- `Reset View`
但没有定义对应函数:
- `window.tlZoom === undefined`
- `window.tlResetView === undefined`
这些按钮应删除、接入同一套 zoom controller,或改成有效按钮。
### C. 客户端缩放算法不完整
当前 wheel handler 更新:
```js
tlData.range_start = fmtDate(newFrom)
tlData.range_end = fmtDate(newTo)
tlData.bucket_count = Math.round(scale)
```
但 segment/event 的 `start_position``display_end_position``position` 都是后端基于旧 range 计算出的 bucket position。只改 `bucket_count` 无法正确表达新 range。缩放后必须重新按时间戳计算位置,或向后端请求基于新 range 的 JSON。
### D. `_partial=1` 合约已变,但旧 partial 模板仍在仓库
当前路由 `_partial=1` 返回 JSON,不再使用 `mission_timeline_partial.html`。但该模板仍存在且是旧 HTML 片段。建议清理或明确废弃,避免后续维护者误用。
### E. 现有 E2E 已过期
`tests/test_e2e.py` 仍验证旧选择器:
- `#tls`
- `.tl-r`
当前实现已经没有这些节点,所以时间线测试失败。需要更新测试,让它验证当前关键行为,而不是旧 DOM 名称。
## 建议修复优先级
### P0
1. 恢复最终 DOM 的时间刻度行,或让客户端 `buildHTML()` 同时渲染刻度。
2. 修复 auto scale:先确定手动 range,再基于最终 range 解析 scale。
3. 删除或实现 `tlZoom()` / `tlResetView()`
4. 让 wheel zoom 要么真正按时间戳重新计算位置,要么改回请求 `_partial=1` JSON 并完整替换数据。
5. 更新 E2E 时间线选择器和断言。
### P1
1. 实现资产 checkbox dropdown,默认全选筛选后的候选资产。
2. 增加 State 筛选。
3. 让 selected count、URL 参数、实际 rows 三者一致。
4. 渲染 event labels、state-node/event 区分和 connector。
### P2
1. 清理废弃的 `mission_timeline_partial.html`
2. 固定测试用 `sim_time`,避免 DB 全局 simulation time 污染测试基线。
3. 给 timeline JSON 增加 schema/契约测试。
## 建议验收用例
1. 打开 `/mission-preview/timeline?sim_time=2060-03-12T09:00`,确认有时间刻度行,首行不是 asset 文本。
2. 默认打开后,asset dropdown 显示所有候选 asset 已选,rows 数等于 selected count。
3. 选择 type/location 后,asset dropdown 只包含筛选结果,并默认全选。
4. 取消一个 asset,确认该 asset row 立即消失或提交后消失。
5.`start=2057-04-24&end=2057-07-21&range_mode=manual&scale=auto`,确认 scale 解析为 Day 或预期精度。
6. 使用滚轮缩放,确认 hidden start/end、刻度标签、segment/event 位置一起变化。
7. 点击 `Zoom +` / `Zoom -` / `Reset View`,确认没有 JS error 且视图变化符合按钮含义。
8. 点击 asset 名称,确认跳到 `/assets/<id>?sim_time=...`
9. Hover event point,确认 tooltip 有 title/time;肉眼可见的事件标签或聚合提示存在。
10. 执行更新后的 E2E,时间线模块不再依赖旧 `#tls` / `.tl-r`
+293
View File
@@ -0,0 +1,293 @@
# Timeline Page Review and Refactor Plan
Date: 2026-05-31
## Context
This project is a Flask/Jinja/SQLAlchemy KSP operations admin app. The original data center still covers engines, communications, tanks, vehicle costs, fuel conversion, workbook import, and Wiki publishing helpers. The recent refactor described in `draft_board.md` expands the app into mission operations:
- `Asset`, `AssetLogEntry`, and `AssetStateNode` are now the core timeline data model.
- Current state, current location, current mission, upcoming events, status board, location board, asset catalog, and timeline are derived from logs at `simulation_time`.
- Logs are split into `state` intervals and `event` points; state intervals may contain timed state nodes.
- Location is intended to move from free text toward a structured hierarchy.
- Timeline is intended to be a read-only navigable view with wheel zoom, drag pan, type/location/state/date filters, and a filtered asset selector.
Current timeline implementation lives mainly in:
- `app/routes/web.py`: `/mission-preview/timeline`, scale/range helpers, row/segment/event building.
- `app/templates/mission_timeline_preview.html`: full page, filter bar, wheel zoom script, tooltip script.
- `app/templates/mission_timeline_partial.html`: AJAX replacement fragment.
- `app/static/styles.css`: legacy `.tl-*` styles plus shared design system.
## Current Timeline Flow
1. The route loads every asset with logs through `_load_assets_with_logs()`.
2. It builds snapshot rows through `_build_mission_board_rows()`.
3. It filters rows by search, `asset_type`, `location`, `record_scope`, and optional `asset_ids`.
4. It resolves the visible time range and scale.
5. `_build_timeline_rows()` converts state intervals into segments and event/state nodes into event markers.
6. The template renders one sticky asset-name column and one scrollable absolute-position lane per asset.
7. Wheel zoom computes a new `start`/`end`, fetches `_partial=1`, parses the returned HTML, and replaces `#tl-canvas.innerHTML`.
## Findings
### 1. Wheel zoom is janky by design
The wheel handler performs a backend request and a large DOM replacement for zoom. A sampled partial request returned about 315 KB in roughly 416 ms and contained 108 segment elements plus 225 event elements. Initial rendered `#tl-canvas` HTML was about 335 KB.
The handler also throttles wheel input with a fixed 400 ms timeout. That makes zoom feel stepped and laggy, especially because each accepted wheel event waits for server render, network, HTML parsing, and full canvas replacement.
Root causes:
- Zoom is server-rendered instead of client-side transformed.
- The whole canvas is replaced, not just scale metadata or a small diff.
- No request cancellation, no stale response guard, and no loading/error state.
- The wheel math uses only the current hidden `start`/`end`, not the cursor position as the zoom anchor.
### 2. Zoom can appear to lose logs/assets
There are two separate problems here.
First, the wheel AJAX URL preserves `q`, `scale`, `asset_type`, and `location`, but it does not preserve `asset_ids` or `record_scope`. I reproduced this by opening the timeline with two selected assets. Before wheel zoom, the page showed 2 rows. After one wheel zoom, the canvas showed 14 rows while the asset multi-select still displayed the original 2 selected assets. The UI state and rendered data diverged.
Second, segment width is rendered incorrectly. The backend computes both `track_width_pct` and `body_width_pct`, but the template uses only `body_width_pct` as the absolute element width. Because `body_width_pct` is usually `100%`, each segment starts at its own left offset and then stretches to the end of the lane. Later segments visually cover earlier ones, which can look like logs disappeared or merged.
Root causes:
- Missing query parameters in wheel AJAX.
- Partial replacement updates only the canvas, not metrics, range display, or selected filter state.
- Template uses `seg.body_width_pct` where it needs the actual timeline span, likely `seg.track_width_pct` on the outer element and `seg.body_width_pct` only on an inner body.
### 3. Scale does not auto-switch between year/month/day
The backend can auto-resolve scale when no `scale` query parameter is supplied. But wheel zoom always sends the current `<select name="scale">` value, so the backend never gets a chance to auto-switch. I reproduced an 88-day visible range that still rendered a single `2057` year label instead of month/day labels.
There is also a template bug: only the Month option has a conditional `selected` attribute. If the route renders `scale=day`, the labels are daily, but the select still displays Year. That means the next wheel AJAX reads `scale=year` from the control and corrupts the intended scale.
Root causes:
- Manual scale and auto scale are conflated.
- Wheel zoom always sends a scale even when the user has not explicitly locked one.
- Year and Day options are missing selected-state rendering.
### 4. Filter model does not match the desired workflow
Desired behavior: first filter by type and location, then show a dropdown multi-select of matching assets, default all selected, and render only selected assets.
Current behavior:
- Type and Location are single native selects.
- Asset selection is a native multi-select with no explicit default selections. Empty selection means "all", but visually it looks like nothing is selected.
- Asset options are derived from filtered rows, but wheel zoom does not preserve selected `asset_ids`.
- State filter is not implemented.
- Location filtering is exact string matching, not hierarchical matching.
- There is no Select All / Clear affordance.
Root causes:
- The route treats asset selection as optional narrowing instead of an explicit selected set.
- Filter state is encoded inconsistently between server render, form controls, and wheel AJAX.
- Location taxonomy is still free-text-derived.
### 5. Event point display is too weak
The backend calculates event label width, label side, label span, event tracks, and state-node connector height. The templates do not render those labels, tracks, or connectors. They render only a 7 px dot at `top:3px` inside a fixed 26 px row.
Effects:
- Many event points collapse into indistinguishable dots.
- State nodes and standalone event points look nearly identical.
- Event labels are only available through hover/title, so scanning the timeline is hard.
- Track assignment does not help because row height is fixed and event track fields are unused.
Root causes:
- The presentation layer is a simplified version of a richer backend layout model.
- Fixed row height prevents multi-track event rendering.
- Event marker design has no density strategy for close events.
### 6. Asset rows do not link to asset detail
The asset name cell is a plain `div` with a `title`. Segments and event dots link to entry edit pages, not asset pages. There is no click target to navigate from a timeline row to `/assets/<asset_id>`.
Root cause:
- `_build_timeline_rows()` includes `asset_id` but not an `asset_url`, and the template does not render the label as an anchor.
## Proposed Refactor
### A. Make timeline state explicit
Create one canonical timeline state object:
- `sim_time`
- `range_start`
- `range_end`
- `scale_mode`: `auto`, `year`, `month`, or `day`
- `asset_type`
- `location`
- `state`
- `asset_ids`
- `record_scope`
- `q`
Rules:
- `scale_mode=auto` lets the backend choose year/month/day from range size.
- Explicit year/month/day locks the scale until the user returns to auto.
- Type/location/state changes rebuild the candidate asset list.
- Candidate assets default to all selected.
- Empty selected assets should mean "no assets selected", not "all", once the new selector is implemented.
### B. Split server data from interaction
Keep the server responsible for canonical data and initial render, but move wheel zoom/pan to a small client-side controller:
- Hydrate timeline rows as JSON or compact data attributes.
- Render positions from `range_start`, `range_end`, and resolved scale in the browser.
- On wheel, update range in memory immediately and re-render positions without a backend round trip.
- Debounce URL synchronization with `history.replaceState`.
- Fetch server data only when filters or simulation time change, or when the visible range moves outside loaded data if pagination/windowing is introduced.
If a full client renderer is too large for this pass, use an intermediate improvement:
- Preserve all query params in AJAX.
- Use `AbortController` to cancel stale partial requests.
- Add a request sequence id so late responses cannot overwrite newer views.
- Replace the whole canvas outer HTML, not only inner HTML, and update metrics/range display/filter hidden fields together.
Recommended path: implement the intermediate fix first, then move to client-side zoom if performance still feels poor.
### C. Fix segment and row rendering
Use the backend track model correctly:
- Render each segment wrapper with `left: seg.left_pct` and `width: seg.track_width_pct`.
- Render an inner segment body with `width: seg.body_width_pct`.
- Use `state_track_count` and `event_track_count` to compute row height.
- Preserve compact labels but do not let a segment occupy the rest of the lane by accident.
### D. Implement the desired filter UX
Filter bar layout:
- Search input.
- Type multi/single filter depending on final preference; for this requirement, type can remain a dropdown filter.
- Location dropdown filter using the future hierarchy matcher when available.
- State dropdown filter.
- Asset dropdown multi-select with checkboxes, Select All, Clear, and selected count.
Data behavior:
- On initial load, after type/location/state filters are applied, all matching assets are selected.
- User deselection is explicit and survives zoom/pan.
- URL serializes selected ids so refresh/share keeps the same view.
- The rendered rows always match the selected assets shown in the selector.
### E. Improve event point design
Recommended event visual model:
- State intervals remain colored horizontal bars.
- State nodes render as small anchored ticks connected to their parent interval, with a tiny label when space allows.
- Standalone event points render as diamond or pin markers, visually distinct from state nodes.
- Event labels use collision tracks when the scale has room; dense views collapse labels into count clusters or show labels only for selected/hovered rows.
- Hover/focus tooltip shows full title, date/time, asset, location/state, and edit link.
### F. Add asset navigation
Add `asset_url` to timeline rows and render the sticky asset label as an anchor:
- Normal click opens asset detail with current `sim_time`.
- Segment/event click keeps existing entry-edit behavior.
- Optional secondary "open asset" affordance can be added in tooltip for segments/events.
## End-to-End Verification Plan
### 1. Static and unit-level checks
- Assert `_resolve_timeline_scale(None, start, end)` returns day/month/year at the intended thresholds.
- Assert explicit `scale=year/month/day` remains locked.
- Assert timeline rows include `asset_url`.
- Assert every segment has `left_pct`, `track_width_pct`, and inner `body_width_pct` with valid percentages.
- Assert filters preserve `asset_ids`, `record_scope`, `q`, type, location, state, range, and scale mode.
### 2. Flask route tests
Use a deterministic fixture with at least:
- 4 assets across 2 types and 2 locations.
- State intervals with overlapping and non-overlapping ranges.
- Standalone event points.
- State nodes inside intervals.
- One retired asset.
Route assertions:
- `/mission-preview/timeline` renders all matching assets selected by default.
- `asset_type + location` filters shrink candidate assets.
- `asset_ids` renders only selected assets.
- `scale_mode=auto` switches from year to month to day as range shrinks.
- Partial responses, if retained, include enough metadata to update metrics and range display.
### 3. Browser E2E checks
Run against local server on `http://127.0.0.1:9000`.
Core cases:
1. Open `/mission-preview/timeline`.
2. Select type and location filters.
3. Verify the asset dropdown contains only matching assets and all are checked.
4. Uncheck one asset.
5. Verify its row disappears and checked count updates.
6. Wheel zoom in repeatedly.
7. Verify visible rows still match checked assets.
8. Verify scale auto-switches from Year to Month to Day at thresholds.
9. Verify no console errors.
10. Click a sticky asset name and verify navigation to `/assets/<id>`.
11. Go back, click a segment/event marker, and verify it still opens the entry edit page.
12. Hover/focus an event marker and verify tooltip contains title and time.
Performance cases:
- Measure accepted wheel-to-DOM-update latency with `performance.now()` in the browser.
- Target: under 50 ms for client-side zoom, or under 150 ms for AJAX fallback on the fixture dataset.
- Verify rapid wheel input does not apply stale responses out of order.
- Verify no full-page navigation happens during wheel zoom.
Visual cases:
- Capture screenshots at 1440x900 and 390x844.
- Verify event labels/markers do not incoherently overlap.
- Verify row heights remain stable and no text spills outside controls.
- Verify year/month/day labels match selected or auto-resolved scale.
### 4. Regression checks for the reported bugs
- Wheel jank: repeated wheel input updates range smoothly without waiting on full server-rendered HTML.
- Lost logs/assets: after selecting 2 assets, wheel zoom still shows exactly those 2 rows.
- Auto scale: an 88-day range shows day labels, a multi-month range shows month labels, a multi-year range shows year labels.
- Filter workflow: type/location filters rebuild the asset dropdown and default to all matching assets selected.
- Event points: standalone events and state nodes are visually distinct and readable.
- Asset navigation: clicking a timeline row label opens the asset detail page.
## Suggested Implementation Order
1. Fix template correctness: selected states for year/day scale and segment width rendering.
2. Preserve all query params in wheel AJAX and guard stale responses.
3. Add `asset_url` and clickable asset labels.
4. Replace native asset multi-select with explicit checkbox dropdown and selected-count model.
5. Add state filter and hierarchical location matching hook.
6. Render event labels/tracks/connectors using the data already computed by the backend.
7. Decide whether to keep AJAX zoom or replace it with a client-side timeline controller.
## Acceptance Criteria
- The timeline never shows a different asset set than the filter UI says is selected.
- Wheel zoom never drops `asset_ids`, `record_scope`, `q`, type, location, or state filters.
- Auto scale and the scale control agree with each other.
- State intervals render at their true clipped span.
- Event points are readable enough to scan without mandatory hover.
- Every asset row has a direct asset-detail navigation path.
- E2E tests cover the six reported issues and fail on the current implementation.