diff --git a/docs/tasks/T-206.md b/docs/tasks/T-206.md new file mode 100644 index 0000000..2bc8c05 --- /dev/null +++ b/docs/tasks/T-206.md @@ -0,0 +1,78 @@ +--- +id: T-206 +title: 单趟状态详情与安全主动作 +phase: 2 +deps: [T-205] +status: TODO +created: 2026-08-04 +vikunja_task_id: 45 +context_ref: cd47d0c +work_branch: task/t-206-status-safe-actions +needs_device: false +needs_human_review: false +write_paths: + - docs/tasks/T-206.md + - admin/internal/taskdetail/** + - admin/internal/server/task_detail.go + - admin/internal/server/task_detail_test.go + - admin/internal/transport/webui/webui.go + - admin/internal/transport/webui/templates/task-detail.html + - docs/routes.md + - docs/08-interaction-checklist.md + - docs/current-state.md +--- + + +## 问题 / 背景 + +T-204 已有只读详情底座,T-205 将提供 attempt 事件、执行摘要和围栏前失败事实。当前详情仍不能可靠回答“目前在哪一步、是否已建立围栏、唯一安全下一步是什么”。T-206 只扩展只读详情和安全主动作推导;不实现重置、调和、付款或设备动作。 + +## 关联需求与交互 + +- 功能:F-004、F-007、F-011、F-017 的状态展示部分。 +- 用户故事:US-002、US-004、US-008。 +- 交互:IX-004、IX-009、IX-010、IX-011 的只读状态和下一步提示。 +- 依赖:T-205;由其传递 T-204、T-210、T-302 及事件/证据事实。 +- 后续消费者:T-207 围栏前人工恢复、T-208 提交围栏与调和。 +- 本任务不新增管理或设备写接口。 + +## 方案 + +1. 在同一个只读事务中加载任务/版本、当前及历史 authorization、T-302 claim 的非秘密摘要、attempt、T-205 append-only 时间线/执行摘要、三闸门 evidence 摘要和 order_submission/调和摘要。设备只展示非秘密名称、generation 和租约状态,不返回 token、nonce 或 session。 +2. 增加纯函数式 SafePrimaryAction 推导。任何 order_submission 的存在都优先于 task.status;其次处理 RECONCILIATION_REQUIRED、WAITING_PAYMENT,再处理围栏前状态。事实互相矛盾时进入 SAFETY_HOLD,不显示写动作。 +3. DRAFT 只引导返回列表勾选,不新增逐行开始按钮;PENDING/CLAIMED/ORDERING 只读展示授权、领取和步骤进度;过期授权只提示等待人工关闭,不能表述为可安全重领。 +4. NEEDS_MANUAL 且无 submission 时显示固定失败码、最后安全步骤和“查看围栏前恢复条件”,本任务只做页内导航;不提前渲染 T-207 的 POST 表单。 +5. 只要存在 submission,即使 task 仍错误地显示 ORDERING/NEEDS_MANUAL,也必须显示围栏锁定和同一 submission;不显示重置、取消、重新授权或再次提交。 +6. RECONCILIATION_REQUIRED 只引导查看同一提交;WAITING_PAYMENT 明确“订单已创建,系统尚未付款”并只引导核对证据;标记付款属于 T-402。 +7. Gate1/Gate2/Gate3 只按服务端固定 kind 和数据库事实展示,不解析图片、不自行计算或推断闸门通过。未知状态、缺少关联记录和归属冲突均 fail closed。 +8. 完整页和抽屉继续共享同一详情模型与模板,保持返回、焦点、滚动、键盘、响应式及 no-store 证据边界。 + +## 验收要点 + +- 覆盖 DRAFT、PENDING、CLAIMED、ORDERING、NEEDS_MANUAL、WAITING_PAYMENT、RECONCILIATION_REQUIRED、终态、空态和 SAFETY_HOLD 的完整矩阵。 +- ORDERING/NEEDS_MANUAL 与 order_submission 同时存在时必须显示围栏锁定;绝不出现围栏前恢复动作。 +- 过期 lease/authorization 只显示事实,不把它推导成手机已停止、可释放或可转领。 +- 页面不出现旧两趟、中途确认、自动付款、重新提交或普通重试术语;DRAFT 不增加逐行开始采购。 +- 完整页和抽屉内容一致,匿名请求在查询前拒绝;页面不泄露 token、nonce、session、SQL 或路径。 +- `go test ./...`、`go test -race ./...`、`go vet ./...`、`go build ./...`、完整 init、上下文、Vikunja export 与 diff-check 通过。 + +## 执行记录 + +(暂无) + + +## 边界 + +- 本任务只读取并展示 T-204/T-205/T-302 已保存的任务、授权、claim、attempt、事件、证据和 + submission 事实;不得新增管理或设备写接口,不得在读取详情时隐式续租、关闭、释放或改变状态。 +- 任意 `order_submission` 的存在都必须压倒 `task.status` 和围栏前提示。即使任务错误地仍为 + `ORDERING` / `NEEDS_MANUAL`,也只能展示同一 submission 的围栏/调和路径,绝不显示重置、取消、 + 重新授权、重新领取或再次提交。 +- 租约或授权过期只是一项只读事实,不证明手机已经停止,不得把它推导成可释放、可转领或可安全 + 重试。事实缺失、归属不完整或互相冲突时进入 `SAFETY_HOLD`,不猜测、不渲染写动作。 +- T-206 的安全主动作只允许返回列表、页内定位或查看既有事实;不得提前实现 T-207 重置/取消、 + T-208 围栏/调和或 T-402 标记付款,不得增加逐行“开始采购”按钮或中途确认。 +- Gate1/Gate2/Gate3 只按服务端固定 kind、事件和数据库记录展示;不解析图片、不自行认定页面、 + 不重新计算为可提交许可。截图仍只供内部审计,不替代实时三闸门。 +- 不接触真机页面判据、client 代码、`submit_order_once()` 或任何支付、免密支付、先用后付和扣款能力。 + diff --git a/docs/tasks/T-207.md b/docs/tasks/T-207.md new file mode 100644 index 0000000..6db2fec --- /dev/null +++ b/docs/tasks/T-207.md @@ -0,0 +1,85 @@ +--- +id: T-207 +title: 授权有效期与围栏前人工恢复 +phase: 2 +deps: [T-206] +status: TODO +created: 2026-08-04 +vikunja_task_id: 46 +context_ref: cd47d0c +work_branch: task/t-207-prefence-recovery +needs_device: false +needs_human_review: false +write_paths: + - docs/tasks/T-207.md + - admin/internal/taskrecovery/** + - admin/internal/taskdetail/** + - admin/internal/server/task_recovery.go + - admin/internal/server/task_recovery_test.go + - admin/internal/server/router.go + - admin/internal/transport/webui/webui.go + - admin/internal/transport/webui/templates/task-detail.html + - admin/cmd/server/** + - admin/README.md + - docs/api.md + - docs/routes.md + - docs/08-interaction-checklist.md + - docs/current-state.md +--- + + +## 问题 / 背景 + +T-205 的围栏前失败只把 attempt 置 FAILED、task 置 NEEDS_MANUAL,并刻意保留 authorization 与 claim;T-302 也明确租约丢失不等于手机停止。T-207 提供管理员人工检查后的围栏前恢复,使旧授权永久关闭后任务回到 DRAFT 或 CANCELED,同时保证任何围栏事实都永远不可释放。 + +## 关联需求与交互 + +- 功能:F-010、F-011、F-017 的围栏前恢复部分。 +- 用户故事:US-005、US-008。 +- 交互:IX-009、IX-010;T-206 负责状态与安全主动作,本任务接通被允许的写操作。 +- 依赖:T-206;由其传递 T-205/T-302 的 claim、attempt 和详情事实。 +- API:POST /tasks/{id}/reset-to-draft、POST /tasks/{id}/cancel。 +- 后续消费者:T-208 submission fence 与恢复/围栏竞态测试。 + +## 方案 + +1. 管理写请求只接受有效管理员 session + CSRF 和有界严格表单;设备 Bearer 无权调用。请求绑定 expected_task_version、authorization_id、可选 expected_attempt_id、可选 expected_claim_generation、必要时的 confirmed_device_stopped 和安全 return_to。 +2. 定义围栏前恢复资格:PENDING 且无 attempt/claim/submission 可关闭;NEEDS_MANUAL 且 T-205 attempt 已明确 FAILED、无 submission 可重置/取消;租约过期或设备撤销但 attempt 曾进入 CLAIMED/ORDERING 时,只有管理员明确确认设备已安全停止才可继续。活跃租约、仍在 ORDERING 且无明确停止事实时拒绝。 +3. authorization/attempt 已 FENCED 或任何关联 order_submission 存在时永久拒绝,不受 task.status、租约、授权过期、人工确认或调和终态影响。数据归属缺失或互相冲突时 fail closed。 +4. 在一个有界 SQLite 写事务中先取得写入线性化位置,再重新读取 task、authorization、attempt、claim 和 submission。首先检查 submission;随后精确比较版本、authorization、attempt 和 generation,最后校验恢复资格。 +5. 成功只做状态关闭,不删除事实:过期 authorization 转 EXPIRED,其余围栏前关闭转 ABANDONED;CLAIMED/ORDERING attempt 可安全关闭为 ABANDONED,已 FAILED 保持 FAILED;claim 留存并只写 closed_at,恢复原因由 authorization/attempt/task 的固定终态组合表达,不新增自由文本字段;task 转 DRAFT 或 CANCELED、version 恰好加一。 +6. 本任务不新增 migration。利用旧 authorization/attempt/claim 终止足迹和 task 版本形成自然幂等:相同旧版本与旧授权的相同终态请求稳定返回原成功;一旦出现新版本/新授权,旧请求必须冲突,不能关闭新授权。 +7. 重置成功不创建新授权。重新开始必须回到列表,由管理员重新点击“开始采购(只创建待付款订单)”并通过 T-203 产生新版本、新授权。 +8. T-206 详情仅在服务端计算确定可恢复时渲染 POST 表单。重置是高影响动作,明确说明旧授权永久关闭和必须重新开始;取消为独立后果,不把两者混为同一动作。 + +## 验收要点 + +- 覆盖无领取 PENDING、明确 FAILED、租约过期但未证明停止、人工停止确认、活跃 ORDERING 及归属冲突。 +- 覆盖 expires_at 边界相等、旧 generation、task/version 漂移、不同 authorization/attempt 和设备撤销。 +- 覆盖响应丢失、服务重启、同请求重放、同键异终态、reset/cancel 并发和新授权后旧请求重放。 +- 覆盖恢复与模拟围栏插入并发:只要任一 order_submission 已存在或先提交,恢复必须零释放;已 MANUAL_RESOLVED 也仍拒绝。 +- 成功后 authorization、claim、attempt、events、evidence 全部保留;只改变获准状态,不删除审计事实。 +- 管理/设备身份隔离、CSRF、请求大小/字段、错误不泄露均有测试。 +- `go test ./...`、`go test -race ./...`、`go vet ./...`、`go build ./...`、完整 init、上下文、Vikunja export 与 diff-check 通过。 + +## 执行记录 + +(暂无) + + +## 边界 + +- 只有有效管理员 session + CSRF 的显式人工动作可以执行围栏前重置或取消;设备 Bearer、后台过期 + 扫描、租约超时、设备撤销、停止轮询和进程重启都不得自动关闭授权或把任务送回 `DRAFT`。 +- 租约过期或设备撤销不证明手机已停止。曾进入 `CLAIMED` / `ORDERING` 且没有 T-205 明确失败事实 + 的 attempt,必须由管理员明确确认设备已安全停止;活跃执行或事实不完整时一律拒绝恢复。 +- 事务必须先取得写入线性化位置,并把任何关联 `order_submission` 的存在作为最高优先级硬拒绝。 + authorization/attempt 已 `FENCED`,或 submission 已 `FENCED`、`SUBMITTED`、 + `RECONCILIATION_REQUIRED`、`MANUAL_RESOLVED`,均不得释放、取消、重置或重新授权。 +- 恢复只能关闭既有围栏前状态并保留全部审计事实;不得删除 authorization、claim、attempt、event、 + evidence 或 submission。重置不得创建新授权,重新开始必须由管理员重新执行 T-203。 +- 自然幂等必须绑定旧 task version、旧 authorization、预期 attempt/generation 和精确终态;不能仅因 + 当前已是 `DRAFT` / `CANCELED` 就返回成功。出现新版本或新授权后,旧请求必须冲突,不能影响新事实。 +- 本任务不建立、查询为许可或模拟 submission fence,不实现设备 result、人工 submission 调和、 + 真机动作、点击“提交订单”或任何支付、免密支付、先用后付和扣款能力。 + diff --git a/docs/tasks/T-208.md b/docs/tasks/T-208.md new file mode 100644 index 0000000..191cccf --- /dev/null +++ b/docs/tasks/T-208.md @@ -0,0 +1,107 @@ +--- +id: T-208 +title: 三闸门提交围栏与结果调和 API +phase: 2 +deps: [T-207, T-210] +status: TODO +created: 2026-08-04 +vikunja_task_id: 47 +context_ref: cd47d0c +work_branch: task/t-208-submission-fence +needs_device: false +needs_human_review: false +write_paths: + - docs/tasks/T-208.md + - admin/migrations/00008_submission_fence.sql + - admin/internal/migrations/migrations_test.go + - admin/internal/domain/submission.go + - admin/internal/domain/submission_test.go + - admin/internal/submission/** + - admin/internal/taskclaim/** + - admin/internal/taskdetail/** + - admin/internal/server/submission_fence.go + - admin/internal/server/submission_fence_test.go + - admin/internal/server/router.go + - admin/internal/transport/webui/webui.go + - admin/internal/transport/webui/templates/task-detail.html + - admin/cmd/server/** + - admin/README.md + - docs/api.md + - docs/04-architecture.md + - docs/routes.md + - docs/current-state.md +--- + + +## 问题 / 背景 + +T-205 将保存同一 attempt 的 append-only 三闸门事件和执行摘要,T-210 将开放 Gate1/Gate2/Gate3 三种固定证据资产,T-207 只允许围栏前人工恢复。真实点击“提交订单”前仍缺少服务端唯一提交围栏、首次一次性许可、围栏响应不明处理、点击后一次性结果以及同一 submission 的人工调和。T-208 只实现采购服务端围栏与调和;不连接真机、不点击提交订单、更不付款。 + +## 关联需求与交互 + +- 功能:F-006、F-007、F-011、F-017。 +- 用户故事:US-004、US-005、US-008。 +- 交互:IX-009、IX-011;围栏由后台申请,Web 只显示和调和同一 submission。 +- 依赖:T-207、T-210;显式保留 T-210 作为三证据类型安全门禁。T-207 传递 T-205/T-302,T-210 传递 T-107/T-204/T-302。 +- API:POST /api/v1/purchase-attempts/{aid}/submission-fence、POST /api/v1/order-submissions/{sid}/result、POST /order-submissions/{sid}/reconcile。 +- 后续消费者:T-401 单趟真实提交。 + +## 方案 + +1. 新增 `00008_submission_fence.sql`,保存 fence request 幂等事实、唯一 order_submission proof、三个 gate evidence 外键、一次性 result request、append-only 人工调和记录,以及冻结的 device/session/claim generation/claim token hash 归属。authorization 与 attempt 各最多一个 submission;降级遇到新事实原子拒绝。 +2. 设备认证必须在读取 body 前完成;严格 JSON 请求绑定 task/version、authorization、attempt、session、generation、claim token、最后事件序号、规格、两次单价、数量、确认页金额、精确 submit_control_text/match_count 和三个 evidence asset id,不接受未知字段、自由页面文本、XML、路径、selector、坐标或动作脚本。 +3. `operation=CREATE_ONCE` 才可能创建围栏。事务必须先取得 SQLite 写线性化位置并再次确认设备 ACTIVE,再复用 T-302 的 current claim validator:设备/session/task/authorization/attempt/generation/token 全部一致,claim lease 与 authorization 严格未过期,task/attempt 为 ORDERING,authorization 为 CLAIMED。当前 validator 不得为结果上报放宽。 +4. 围栏事务逐字段比对请求、T-205 append-only 事件、purchase attempt summary 与 T-210 evidence assets。三个资产必须精确为 SKU_PANEL_GATE_1、SKU_PANEL_GATE_2、ORDER_CONFIRM_GATE_3,属于同一 task/attempt/device;服务端只验证 kind/归属/hash 事实,不解析图片内容。 +5. 服务端重新执行三闸门:颜色/尺码精确等于授权;Gate1/Gate2 为规范十进制正金额且完全相等;Gate1 × 授权数量不超 total_price_cap;数量精确相等;Gate3 规格/数量一致且确认页总额不超上限;submit_control_text 精确等于“提交订单”,可点击祖先计数精确为一。任一缺失、歧义、顺序错误或冲突都零围栏、零许可。 +6. 首次 CREATE_ONCE 在一个事务中创建唯一 submission,并把 authorization CLAIMED→FENCED、attempt ORDERING→FENCED,使 task 永久不可重领。只有这次首次明确成功响应可含 `click_permitted=true`;许可不是数据库中可重取的开关,也不能由业务代码手工构造。 +7. 同 fence_key 同规范 proof 的任何重放都返回同一 submission_id,但必须 `click_permitted=false`、`reconciliation_required=true`;同 key 异 proof、不同 key 命中同 authorization/attempt、并发失败或服务重启都绝不再次许可。 +8. `operation=LOOKUP_ONLY` 只用于围栏响应不明后的同键调和:永远不创建 submission、永远不许可点击。找到原围栏时返回同一 submission 并保持/转入调和;找不到时明确无围栏事实,但恢复路径仍不得重新申请首次点击许可。 +9. 围栏后的 result 不再要求活跃租约,也不得放宽 current claim validator;新增独立 fenced ownership validator,只按 order_submission 冻结的 device/session/generation/token hash 验证原设备归属。它不能创建、续租、释放或转领 claim。 +10. 每个 submission 只接受一条一次性 result 事实。SUBMITTED 转 WAITING_PAYMENT 并永久消费授权;EXTERNAL_PAYMENT_HANDOFF、SECURITY_CHALLENGE、UNKNOWN 转 RECONCILIATION_REQUIRED 并保持围栏。相同 result_key 同载荷稳定重放,不同 key/observation 不得覆盖。没有 retry observation。 +11. 本任务从 result wire 移除旧 `evidence_asset_id`。首次真实提交后的页面尚未经本项目真机取证,不新增 SUBMISSION_RESULT kind;UNKNOWN 允许无截图。绝不把 Gate3 图冒充结果图,绝不上传外部支付页、支付凭据或完整 XML。后续结果截图必须另立取证和 evidence kind 任务。 +12. 管理调和只处理指定 submission,结论固定为 CONFIRMED_CREATED、CONFIRMED_NOT_CREATED_OR_CANNOT_COMPLETE、STILL_UNKNOWN;记录 reviewer、固定结论、服务端时间和幂等键,不触发设备动作。前两种形成最终人工结论后 authorization 永久 CONSUMED;STILL_UNKNOWN 保持围栏和待调和。 +13. T-206 详情显示同一 submission 的 proof、首次许可事实、result 和调和记录。任何围栏后页面都没有重置、取消、重新授权、重新领取、再次提交或普通重试入口;WAITING_PAYMENT 仍明确系统尚未付款。 + +## 验收要点 + +- 两设备、同设备、同/异 key 并发中最多一个首次响应拥有 `click_permitted=true`;事务回滚时零 submission/状态变化。 +- 覆盖首次成功、同键重放、响应截断、服务重启、异载荷/异 key 冲突;所有非首次路径 click_permitted 都为 false。 +- LOOKUP_ONLY 找到/未找到、并发和重启都零创建、零许可;围栏响应不明不通过新 key 或恢复模式获得许可。 +- 覆盖错误/缺失/跨 task/attempt/device 的三类 evidence、事件缺口/乱序、summary 冲突,以及规格、数量、金额、上限、提交文本和 match count 全部负例。 +- 覆盖 current claim validator 的 lease/authorization 边界;静态和运行测试证明 fenced ownership validator 只验证冻结归属,不能被用于领取、续租、围栏创建或释放。 +- 覆盖 fence 与 T-207 reset/cancel 并发;任一 submission 一旦存在,所有 authorization 状态和人工结论下都永久拒绝释放。 +- 覆盖四种 result、同键重放、异 observation 冲突、UNKNOWN 无截图,以及三种人工调和结论;任何结果均不开放第二次点击。 +- Migration 覆盖升级/重开、唯一/FK/trigger、已有安全事实、降级拒绝和 foreign_key_check。 +- 静态检查没有 client/PDD selector、通用点击、submit_order_once、结果截图 kind、支付、免密支付、先用后付或扣款能力。 +- `go test ./...`、`go test -race ./...`、`go vet ./...`、`go build ./...`、完整 init、上下文、Vikunja export 与 diff-check 通过。 + +## 执行记录 + +(暂无) + + +## 边界 + +- `CREATE_ONCE` 建立围栏时必须复用 T-302 的 **current claim validator**,严格要求当前设备、session、 + task、authorization、attempt、generation、claim token、未过期 lease/authorization 与当前状态 + 全部一致。不得为了让围栏或结果流程跑通而放宽、复活或绕过该 validator。 +- 围栏后的 result 必须使用独立 **fenced ownership validator**,只验证 order_submission 已冻结的 + device/session/generation/token hash 归属。它不得被用于领取、续租、创建第二个围栏、释放、转领, + 也不得反向削弱 current claim validator。 +- 只有首次 `CREATE_ONCE` 原子创建唯一 submission 的明确成功响应可以包含 + `click_permitted=true`。同 key 重放、异 key 冲突、并发输家、服务重启、响应不明恢复及任何 + `LOOKUP_ONLY` 都必须 `click_permitted=false`;许可不得作为可重取数据库开关或手工构造对象。 +- `LOOKUP_ONLY` 永远零创建、零许可。围栏响应不明时不得换 key、再次申请首次许可或凭页面猜测点击; + 找到原围栏只能调和同一 submission,找不到也只能按围栏前安全失败处置。 +- 围栏事务必须逐字段比对请求、T-205 append-only events/summary 和 T-210 三类 evidence 资产, + 并在服务端重算三闸门与精确唯一“提交订单”控件。缺失、歧义、跨 task/attempt/device、价格/规格/ + 数量冲突或事件乱序均零围栏、零许可。 +- 本任务不新增 `SUBMISSION_RESULT` 或其他结果截图 kind,并从 result wire 移除旧 + `evidence_asset_id`。首次真实提交后的页面尚未完成本项目真机取证;`UNKNOWN` 允许无截图,不得把 + Gate3 图片冒充结果图,外部支付页、支付凭据和完整 XML 永不上传。 +- 任一 submission 建立后,authorization、attempt 和 submission 永久不可释放、重置、取消、重新 + 授权、重新领取或再次点击。result 或人工调和只能收紧到 `WAITING_PAYMENT` / `FAILED` / 保持 + `RECONCILIATION_REQUIRED`,绝不能回到围栏前状态。 +- 本任务只实现服务端围栏、一次性结果和同一 submission 调和;不修改 client/PDD 页面判据,不连接 + 真机,不实现 `submit_order_once()`,不点击“提交订单”,不编写支付、免密支付、先用后付或扣款能力。 +