Skip to content

feat: complete command workspace task, map, and summary modules - #151

Merged
cg8-5712 merged 12 commits into
mainfrom
feature/issue-80-map-situation
Jul 30, 2026
Merged

cg8-5712 merged 12 commits into
mainfrom
feature/issue-80-map-situation

Conversation

@cg8-5712

@cg8-5712 cg8-5712 commented Jul 29, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • add the commander task board with manual, authorized volunteer assignment; role-bound status transitions; and pending-review volunteer feedback
  • add a role-filtered map situation view with text fallback, precision/status/source metadata, and controlled POI usage
  • add authorized deterministic case-summary visibility alongside the existing review-and-publish workflow
  • document the new commander-only case-member listing endpoint in OpenAPI and API documentation

Validation

  • npm run build --prefix frontend
  • npm run lint --prefix frontend
  • npm run test --prefix frontend -- --run
  • cargo test --test api_contract

Closes #24
Closes #29
Closes #31
Closes #77
Closes #78
Closes #79
Closes #80
Closes #81

Summary by CodeRabbit

  • 新功能
    • 指挥端新增待受理队列接案流程,支持案件地图态势、任务看板与指挥协作(含受理与任务反馈重载)。
    • 新增案件成员列表与最新摘要草稿读取;摘要草稿支持刷新与审核发布(待审核时继续填写审核理由)。
    • 仪表盘新增“待受理案件”统计与展示。
  • 安全与权限
    • 强化家属端路由/导航仅限家庭成员可见,并限制家属邀请范围;指挥端成员与摘要草稿访问按案件权限控制。
    • 重复接案返回冲突状态。
  • 安全与行为调整
    • 线索添加冲突语义调整:仅允许向进行中案件添加线索。
    • 线索审阅时摘要草稿生成失败不影响审阅结果。
  • 测试
    • 扩展接口契约与指挥/家属场景用例,覆盖页面交互与状态门控。
  • 文档
    • 更新 API 文档与 OpenAPI:新增队列接案、成员列表、最新摘要草稿等端点与字段约束。

cg8-5712 added 4 commits July 29, 2026 22:14
- add typed map-view API models and client request for case map data
- load map records with request versioning to prevent stale responses
- render text-based situation cards for places, clues, tasks, and last-seen data
- add object-type filtering, coordinate availability counts, and refresh support
- expose precision, review status, source, and event timing for each record
- provide loading, empty, and error states for map service degradation
- cover map-view endpoint usage and text fallback rendering in tests
- expose commander-only endpoint to list current case members
- document member listing in API reference and OpenAPI specification
- add frontend task types and APIs for listing, creating, and updating tasks
- render task board with volunteer status controls and commander cancellation
- allow commanders to assign confirmed-clue tasks to authorized volunteers
- add case summary and clue response interfaces
- add API client for the case summary endpoint
- load and reset summaries with case workspace state
- show confirmed information, pending verification, and safety reminders
- provide a commander-only summary refresh action
- add API client for posting task feedback to the review endpoint
- show feedback input for volunteers with active tasks
- submit feedback as pending review without creating confirmed facts
- clear submitted feedback and handle request loading and errors
Copilot AI review requested due to automatic review settings July 29, 2026 14:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

新增案件接管、成员查询、任务协作、地图态势和授权摘要流程,并同步完善前后端 API 契约、权限控制、家庭工作区限制及相关测试。

Changes

案件工作区协作能力

Layer / File(s) Summary
案件接管与成员查询服务
src/api/models.rs, src/api/routes/cases.rs, src/application/services/case_service.rs, tests/api/case_members_post.rs
新增最小待受理队列、原子接管、案件成员查询,并调整家属成员邀请角色限制。
案件 API 类型与调用契约
frontend/src/api/cases.ts, frontend/src/api/cases.test.ts, docs/openapi.yaml, docs/API.md
新增命令接管、地图、摘要、任务和成员类型及 API 方法,补充字段裁剪、elder_age 与摘要草稿契约。
任务看板、地图态势与授权摘要
frontend/src/pages/CaseWorkspacePage.tsx, frontend/src/pages/CaseWorkspacePage.test.tsx, src/application/services/case_collaboration_service.rs, src/api/routes/clues.rs, tests/api/clue_review_patch.rs, docs/API.md
工作区支持任务分配与状态流转、反馈提交、地图文字地点展示、授权摘要加载,以及线索审核后自动生成待审核摘要草稿。
指挥接案总览与家庭访问控制
frontend/src/pages/DashboardPage.tsx, frontend/src/pages/DashboardPage.test.tsx, frontend/src/App.tsx, frontend/src/components/AppShell.tsx, frontend/src/App.test.tsx
指挥用户在总览查看待受理案件;家庭路由和导航仅允许无全局能力的家庭成员访问与显示。
线索状态约束
src/application/services/case_service.rs, tests/api/case_clues_post.rs, docs/API.md, docs/openapi.yaml
新增线索仅允许提交到 active 案件,并覆盖非活动案件的冲突响应。

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Commander
  participant DashboardPage
  participant CaseWorkspacePage
  participant cases_api
  participant Backend
  Commander->>DashboardPage: 打开指挥总览
  DashboardPage->>cases_api: 请求案件列表与待受理队列
  cases_api->>Backend: GET /api/cases/command-intake
  Backend-->>cases_api: 返回最小接案信息
  cases_api-->>DashboardPage: 返回待受理案件
  Commander->>CaseWorkspacePage: 选择并受理案件
  CaseWorkspacePage->>cases_api: POST /api/cases/{case_id}/accept-command
  cases_api->>Backend: 创建 commander 成员关系并记录审计
  Backend-->>CaseWorkspacePage: 返回 CaseDetail
  CaseWorkspacePage->>cases_api: 请求任务、成员、地图和授权摘要
  cases_api->>Backend: 调用案件协作 API
  Backend-->>CaseWorkspacePage: 返回角色裁剪数据
Loading

Possibly related PRs

  • AG-Angui/Angui#94:同样强化 OpenAPI 与 API 文档中的鉴权和数据可见性契约。
  • AG-Angui/Angui#130:提供本次前端任务生命周期 API 调用依赖的后端接口。
  • AG-Angui/Angui#141:提供本次工作区使用的角色裁剪地图视图接口与响应模型。
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive 变更覆盖了[#24 #29 #31 #77]的主要接口和界面,但仅凭摘要无法确认[#78 #79]的完整证据链、状态机和发布/回退要求已全部满足。 补充这些功能的实现与测试证据,尤其是任务内位置会话、线索审核字段链路,以及摘要发布/撤销/回退生命周期。
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 标题概括了指挥端任务、地图和摘要模块的主要改动,且与本次变更一致。
Out of Scope Changes check ✅ Passed 变更总体围绕指挥端任务、地图、摘要和权限收敛,未见明显与目标无关的扩展。
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/issue-80-map-situation

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

Preview ready

The preview for this build is available at https://pr-151-5f3c338a8005.angui.cg8.site.

Temporary demo accounts (all use the password 37138578d0c6b196346fcc06b627a071ca272e78b3393fa0):

Role Email
Family family@demo.invalid
Commander commander@demo.invalid
Volunteer volunteer@demo.invalid
Learner learner@demo.invalid
Administrator admin@demo.invalid

These accounts and all preview data belong only to this deployment. The next preview deployment resets the SQLite database and invalidates these credentials.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/pages/CaseWorkspacePage.test.tsx (1)

6-38: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

mock 工厂缺少新增 API 函数,导致任务看板/摘要相关调用在所有 commander 模式测试中静默抛错

vi.mock('../api/cases', ...) 的返回对象未包含 listCaseTasks、listCaseMembers、getCaseSummary、createCaseTask、updateTaskStatus、submitTaskFeedback,而 CaseWorkspacePage.tsx 的 TaskBoard/CaseCollaborationPanel 挂载时会调用其中若干函数。由于 vi.mock 会完全替换模块导出,未列出的导出在测试中即为 undefined,调用会抛出 TypeError: ... is not a function。这个异常会被组件内部 try/catch 吞掉并塞进各自的本地错误状态,因此不会让测试直接失败,但意味着本文件里所有 mode="commander" 的测试(包括本次新增的地图态势测试)实际上从未验证过任务看板与授权摘要这两个新功能的集成是否正确,也会在每次渲染时静默产生额外的内部错误状态。

♻️ 建议补全 mock
 const mocked = vi.hoisted(() => ({
   auth: { token: 'test-session' as string | null },
   getCase: vi.fn(),
   getCaseMapView: vi.fn(),
+  getCaseSummary: vi.fn(),
+  listCaseTasks: vi.fn(),
+  listCaseMembers: vi.fn(),
+  createCaseTask: vi.fn(),
+  updateTaskStatus: vi.fn(),
+  submitTaskFeedback: vi.fn(),
   getCasePublicProgress: vi.fn(),
   getCaseResourceConfiguration: vi.fn(),
   listCases: vi.fn(),
   listCaseClues: vi.fn(),
   listCasePois: vi.fn(),
   addCaseMember: vi.fn(),
   reviewClue: vi.fn(),
 }))
 ...
 vi.mock('../api/cases', () => ({
   getCase: (...args: unknown[]) => mocked.getCase(...args),
   getCaseMapView: (...args: unknown[]) => mocked.getCaseMapView(...args),
+  getCaseSummary: (...args: unknown[]) => mocked.getCaseSummary(...args),
+  listCaseTasks: (...args: unknown[]) => mocked.listCaseTasks(...args),
+  listCaseMembers: (...args: unknown[]) => mocked.listCaseMembers(...args),
+  createCaseTask: (...args: unknown[]) => mocked.createCaseTask(...args),
+  updateTaskStatus: (...args: unknown[]) => mocked.updateTaskStatus(...args),
+  submitTaskFeedback: (...args: unknown[]) => mocked.submitTaskFeedback(...args),
   ...
 }))

建议默认 mockResolvedValue 为空数组/空分页,避免为每个既有测试都补参数。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/CaseWorkspacePage.test.tsx` around lines 6 - 38, 补全
CaseWorkspacePage.test.tsx 中 api/cases 的 vi.mock 工厂,加入
listCaseTasks、listCaseMembers、getCaseSummary、createCaseTask、updateTaskStatus 和
submitTaskFeedback 的 mocked 实现;为读取接口设置默认空数组或空分页的 resolved value,并为写入接口提供可解析的默认
mock,确保 TaskBoard 与 CaseCollaborationPanel 在 commander 模式测试中正常挂载且不产生被吞掉的
TypeError。
🧹 Nitpick comments (1)
src/application/services/case_service.rs (1)

814-828: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

逐成员串行查询造成 N+1

for membership in memberships 内对每个成员分别调用 users::Entity::find_by_id 和 global_capabilities_for_user,共产生 2N+1 次数据库查询。案件成员数量增长后会线性增加数据库往返次数。可考虑先收集 user_id 列表,用 Column::Id.is_in(user_ids) 和对应的能力表批量查询后在内存中做映射拼装,将查询次数降为常数级。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/application/services/case_service.rs` around lines 814 - 828,
重构包含成员组装逻辑的函数,避免在 `for membership in memberships` 循环中逐个调用
`users::Entity::find_by_id` 和 `global_capabilities_for_user`。先收集所有 `user_id`,使用
`Column::Id.is_in(user_ids)` 批量查询用户及对应能力数据,再通过内存映射构建
`CaseMemberResponse`,将数据库查询次数降为常数级并保留现有错误与字段转换行为。
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/pages/CaseWorkspacePage.tsx`:
- Line 787: Update the volunteer feedback section in CaseWorkspacePage so it
renders and allows submission only when both task.status and the case
detail.status are active. Guard the existing active-task feedback form with
detail.status === 'active', while preserving the current feedback state,
validation, and sendFeedback flow.
- Around line 761-768: Update TaskBoard.load in
frontend/src/pages/CaseWorkspacePage.tsx:761-768 to use a requestVersion ref,
increment or capture the request version consistently with the file’s existing
loader pattern, and only apply setTasks/setMembers when the response version is
current. Apply the same stale-response protection to loadSummary at
frontend/src/pages/CaseWorkspacePage.tsx:931-934, guarding setSummary; both
sites require direct changes.
- Around line 1014-1017: Update the read-only summary JSX in CaseWorkspacePage
around the summary render to display the existing CaseSummary fields
excluded_directions, task_status, and current_focus, alongside confirmed_clues
where applicable. Add clear labeled sections distinguishing confirmed
information, pending verification, excluded directions, task status/current
focus, and safety_reminders, with appropriate empty-state text while preserving
the existing generated_at and source_scope metadata.
- Line 788: Update the risk_notes field handler in the manual task form so
safety_briefing is assigned the current event.target.value on every change
instead of reusing the existing draft value. Keep risk_notes synchronized with
the same complete text so createCaseTask persists and displays the full safety
briefing.

---

Outside diff comments:
In `@frontend/src/pages/CaseWorkspacePage.test.tsx`:
- Around line 6-38: 补全 CaseWorkspacePage.test.tsx 中 api/cases 的 vi.mock 工厂,加入
listCaseTasks、listCaseMembers、getCaseSummary、createCaseTask、updateTaskStatus 和
submitTaskFeedback 的 mocked 实现;为读取接口设置默认空数组或空分页的 resolved value,并为写入接口提供可解析的默认
mock,确保 TaskBoard 与 CaseCollaborationPanel 在 commander 模式测试中正常挂载且不产生被吞掉的
TypeError。

---

Nitpick comments:
In `@src/application/services/case_service.rs`:
- Around line 814-828: 重构包含成员组装逻辑的函数,避免在 `for membership in memberships` 循环中逐个调用
`users::Entity::find_by_id` 和 `global_capabilities_for_user`。先收集所有 `user_id`,使用
`Column::Id.is_in(user_ids)` 批量查询用户及对应能力数据,再通过内存映射构建
`CaseMemberResponse`,将数据库查询次数降为常数级并保留现有错误与字段转换行为。
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9a81de4c-c3de-42fe-a420-185122a29153

📥 Commits

Reviewing files that changed from the base of the PR and between f0d5d65 and 6dd566b.

📒 Files selected for processing (8)
  • docs/API.md
  • docs/openapi.yaml
  • frontend/src/api/cases.test.ts
  • frontend/src/api/cases.ts
  • frontend/src/pages/CaseWorkspacePage.test.tsx
  • frontend/src/pages/CaseWorkspacePage.tsx
  • src/api/routes/cases.rs
  • src/application/services/case_service.rs

Comment on lines +761 to +768
const load = useCallback(async () => {
if (!token) return
setLoading(true); setError('')
try {
const [page, nextMembers] = await Promise.all([listCaseTasks(token, detail.id), isCommander ? listCaseMembers(token, detail.id) : Promise.resolve([])])
setTasks(page.items); setMembers(nextMembers)
} catch (cause) { setError(messageFrom(cause)) } finally { setLoading(false) }
}, [detail.id, isCommander, token])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

两处新增加载函数遗漏了本文件已建立的“过期响应”防护模式

本文件对每个会随案件切换而重新拉取数据的 loader 都使用了 requestVersion/*RequestVersion 引用来丢弃切换案件后才返回的过期响应(loadDetail、loadClueQueue、loadPublicProgress、CaseSituationPanel.loadMapView,并有对应测试如“does not let an earlier detail request overwrite the most recently selected case”“discards a stale family public-progress response after switching cases”)。这次新增的两个 loader 没有沿用该模式:快速切换案件时,旧案件的任务/成员/摘要响应可能在新案件渲染后才 resolve,从而用旧案件数据覆盖当前案件的任务看板或授权摘要,属于跨案件的过期数据展示问题。

  • frontend/src/pages/CaseWorkspacePage.tsx#L761-L768:为 TaskBoard.load 增加 requestVersion 引用,仅在版本匹配时才 setTasks/setMembers。
  • frontend/src/pages/CaseWorkspacePage.tsx#L931-L934:为 loadSummary 增加同样的版本防护,仅在版本匹配时才 setSummary。
📍 Affects 1 file
  • frontend/src/pages/CaseWorkspacePage.tsx#L761-L768 (this comment)
  • frontend/src/pages/CaseWorkspacePage.tsx#L931-L934
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/CaseWorkspacePage.tsx` around lines 761 - 768, Update
TaskBoard.load in frontend/src/pages/CaseWorkspacePage.tsx:761-768 to use a
requestVersion ref, increment or capture the request version consistently with
the file’s existing loader pattern, and only apply setTasks/setMembers when the
response version is current. Apply the same stale-response protection to
loadSummary at frontend/src/pages/CaseWorkspacePage.tsx:931-934, guarding
setSummary; both sites require direct changes.

<section className="border-b border-slate-200 px-5 py-5 sm:px-6" aria-label="任务看板">
<div className="flex items-start justify-between gap-3"><div><h3 className="m-0 text-base font-bold text-slate-950">任务看板</h3><p className="mb-0 mt-1 text-xs leading-5 text-slate-600">任务只由指挥人工创建和分配;状态、人员与失败结果均以服务端返回为准。</p></div><Button size="sm" variant="ghost" isDisabled={loading} onPress={() => void load()}><RefreshCw size={16} />刷新任务</Button></div>
{error && <div className="mt-3"><Message tone="error">{error}</Message></div>}
{loading ? <p className="mb-0 mt-3 text-sm text-slate-500">正在加载任务…</p> : tasks.length === 0 ? <p className="mb-0 mt-3 text-sm text-slate-500">当前没有你可查看的任务。</p> : <div className="mt-3 grid gap-3 lg:grid-cols-2">{tasks.map((task) => <article key={task.id} className="rounded-md border border-slate-200 bg-white p-3"><div className="flex flex-wrap items-center gap-2"><Chip size="sm" variant="soft"><Chip.Label>{taskStatusLabels[task.status]}</Chip.Label></Chip><Chip size="sm" variant="soft"><Chip.Label>{task.risk_level} 风险</Chip.Label></Chip></div><h4 className="mb-0 mt-3 text-sm font-semibold text-slate-950">{task.title}</h4><p className="mb-0 mt-1 text-sm text-slate-700">{task.objective}</p><p className="mb-0 mt-2 text-xs text-slate-500">区域:{task.area_text} · 截止:{formatDate(task.due_at) ?? '待补充'}</p><p className="mb-0 mt-1 text-xs text-slate-600">安全提示:{task.safety_briefing}</p>{detail.access_role === 'volunteer' && <><div className="mt-3 flex flex-wrap gap-2">{task.status === 'assigned' && <Button size="sm" variant="secondary" isDisabled={busy === task.id} onPress={() => void changeStatus(task.id, 'accepted')}>接受</Button>}{task.status === 'accepted' && <Button size="sm" variant="secondary" isDisabled={busy === task.id} onPress={() => void changeStatus(task.id, 'active')}>开始执行</Button>}{task.status === 'active' && <><Button size="sm" variant="secondary" isDisabled={busy === task.id} onPress={() => void changeStatus(task.id, 'blocked')}>标记受阻</Button><Button size="sm" variant="primary" isDisabled={busy === task.id} onPress={() => void changeStatus(task.id, 'completed')}>完成</Button></>}{task.status === 'blocked' && <Button size="sm" variant="secondary" isDisabled={busy === task.id} onPress={() => void changeStatus(task.id, 'active')}>继续执行</Button>}</div>{task.status === 'active' && <div className="mt-3"><TextArea aria-label={`${task.title} 任务反馈`} value={feedback[task.id] ?? ''} maxLength={4000} rows={2} placeholder="提交现场反馈;将进入人工审核,不会直接成为确认事实" onChange={(event) => setFeedback((current) => ({ ...current, [task.id]: event.target.value }))} fullWidth /><div className="mt-2 flex justify-end"><Button size="sm" variant="secondary" isDisabled={!feedback[task.id]?.trim() || busy === `feedback-${task.id}`} onPress={() => void sendFeedback(task.id)}>提交待审核反馈</Button></div></div>}</>}{isCommander && !['completed', 'cancelled'].includes(task.status) && <div className="mt-3"><Button size="sm" variant="ghost" isDisabled={busy === task.id} onPress={() => void changeStatus(task.id, 'cancelled')}>取消任务</Button></div>}</article>)}</div>}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

志愿者反馈框未校验案件状态

反馈输入区仅按 task.status === 'active' 显示,但 docs/API.md 明确要求“仅允许该任务受领志愿者在任务和案件均为 active 时提交”。若案件已变为 resolved/closed 而任务仍是 active,UI 仍会展示反馈框,提交后才会被服务端拒绝,与文件中其它表单统一用 detail.status !== 'closed'(甚至更严格的 active 判断)做前置校验的风格不一致。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/CaseWorkspacePage.tsx` at line 787, Update the volunteer
feedback section in CaseWorkspacePage so it renders and allows submission only
when both task.status and the case detail.status are active. Guard the existing
active-task feedback form with detail.status === 'active', while preserving the
current feedback state, validation, and sendFeedback flow.

<div className="flex items-start justify-between gap-3"><div><h3 className="m-0 text-base font-bold text-slate-950">任务看板</h3><p className="mb-0 mt-1 text-xs leading-5 text-slate-600">任务只由指挥人工创建和分配;状态、人员与失败结果均以服务端返回为准。</p></div><Button size="sm" variant="ghost" isDisabled={loading} onPress={() => void load()}><RefreshCw size={16} />刷新任务</Button></div>
{error && <div className="mt-3"><Message tone="error">{error}</Message></div>}
{loading ? <p className="mb-0 mt-3 text-sm text-slate-500">正在加载任务…</p> : tasks.length === 0 ? <p className="mb-0 mt-3 text-sm text-slate-500">当前没有你可查看的任务。</p> : <div className="mt-3 grid gap-3 lg:grid-cols-2">{tasks.map((task) => <article key={task.id} className="rounded-md border border-slate-200 bg-white p-3"><div className="flex flex-wrap items-center gap-2"><Chip size="sm" variant="soft"><Chip.Label>{taskStatusLabels[task.status]}</Chip.Label></Chip><Chip size="sm" variant="soft"><Chip.Label>{task.risk_level} 风险</Chip.Label></Chip></div><h4 className="mb-0 mt-3 text-sm font-semibold text-slate-950">{task.title}</h4><p className="mb-0 mt-1 text-sm text-slate-700">{task.objective}</p><p className="mb-0 mt-2 text-xs text-slate-500">区域:{task.area_text} · 截止:{formatDate(task.due_at) ?? '待补充'}</p><p className="mb-0 mt-1 text-xs text-slate-600">安全提示:{task.safety_briefing}</p>{detail.access_role === 'volunteer' && <><div className="mt-3 flex flex-wrap gap-2">{task.status === 'assigned' && <Button size="sm" variant="secondary" isDisabled={busy === task.id} onPress={() => void changeStatus(task.id, 'accepted')}>接受</Button>}{task.status === 'accepted' && <Button size="sm" variant="secondary" isDisabled={busy === task.id} onPress={() => void changeStatus(task.id, 'active')}>开始执行</Button>}{task.status === 'active' && <><Button size="sm" variant="secondary" isDisabled={busy === task.id} onPress={() => void changeStatus(task.id, 'blocked')}>标记受阻</Button><Button size="sm" variant="primary" isDisabled={busy === task.id} onPress={() => void changeStatus(task.id, 'completed')}>完成</Button></>}{task.status === 'blocked' && <Button size="sm" variant="secondary" isDisabled={busy === task.id} onPress={() => void changeStatus(task.id, 'active')}>继续执行</Button>}</div>{task.status === 'active' && <div className="mt-3"><TextArea aria-label={`${task.title} 任务反馈`} value={feedback[task.id] ?? ''} maxLength={4000} rows={2} placeholder="提交现场反馈;将进入人工审核,不会直接成为确认事实" onChange={(event) => setFeedback((current) => ({ ...current, [task.id]: event.target.value }))} fullWidth /><div className="mt-2 flex justify-end"><Button size="sm" variant="secondary" isDisabled={!feedback[task.id]?.trim() || busy === `feedback-${task.id}`} onPress={() => void sendFeedback(task.id)}>提交待审核反馈</Button></div></div>}</>}{isCommander && !['completed', 'cancelled'].includes(task.status) && <div className="mt-3"><Button size="sm" variant="ghost" isDisabled={busy === task.id} onPress={() => void changeStatus(task.id, 'cancelled')}>取消任务</Button></div>}</article>)}</div>}
{isCommander && detail.status !== 'closed' && <form className="mt-5 grid gap-3 border-t border-slate-200 pt-5 lg:grid-cols-2" onSubmit={(event) => { event.preventDefault(); if (!token) return; setBusy('create'); setError(''); createCaseTask(token, detail.id, draft).then(async () => { setDraft(emptyTask()); await load() }).catch((cause) => setError(messageFrom(cause))).finally(() => setBusy('')) }}><h4 className="m-0 text-sm font-semibold text-slate-950 lg:col-span-2">人工创建并分配任务</h4><Field label="关联的已确认线索" required><select required className="min-h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm" value={draft.source_clue_id} onChange={(event) => setDraft({ ...draft, source_clue_id: event.target.value })}><option value="">请选择</option>{confirmedClues.map((clue) => <option key={clue.id} value={clue.id}>{clue.content.slice(0, 80)}</option>)}</select></Field><Field label="志愿者" required><select required className="min-h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm" value={draft.volunteer_user_id} onChange={(event) => setDraft({ ...draft, volunteer_user_id: event.target.value })}><option value="">请选择</option>{volunteers.map((member) => <option key={member.user_id} value={member.user_id}>{member.display_name}</option>)}</select></Field><Field label="任务标题" required><Input required value={draft.title} maxLength={200} onChange={(event) => setDraft({ ...draft, title: event.target.value })} fullWidth /></Field><Field label="任务区域" required><Input required value={draft.area_text} maxLength={500} onChange={(event) => setDraft({ ...draft, area_text: event.target.value })} fullWidth /></Field><Field label="目标" required><TextArea required value={draft.objective} maxLength={4000} rows={2} onChange={(event) => setDraft({ ...draft, objective: event.target.value })} fullWidth /></Field><Field label="截止时间" required><Input required type="datetime-local" value={draft.due_at} onChange={(event) => setDraft({ ...draft, due_at: event.target.value ? new Date(event.target.value).toISOString() : '' })} fullWidth /></Field><Field label="背景说明" required><TextArea required value={draft.background} maxLength={10000} rows={2} onChange={(event) => setDraft({ ...draft, background: event.target.value })} fullWidth /></Field><Field label="风险说明与安全提示" required><TextArea required value={draft.risk_notes} maxLength={4000} rows={2} onChange={(event) => setDraft({ ...draft, risk_notes: event.target.value, safety_briefing: draft.safety_briefing || event.target.value })} fullWidth /></Field><Field label="预期反馈" required><TextArea required value={draft.expected_feedback} maxLength={4000} rows={2} onChange={(event) => setDraft({ ...draft, expected_feedback: event.target.value })} fullWidth /></Field><div className="flex items-end"><Button type="submit" size="sm" variant="primary" isDisabled={busy === 'create' || confirmedClues.length === 0 || volunteers.length === 0}>创建并分配</Button></div></form>}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

创建任务表单中 safety_briefing 逻辑错误,会被“卡”在首字符

risk_notes 的 onChange 里:safety_briefing: draft.safety_briefing || event.target.value。首次输入后 draft.safety_briefing 变为非空字符串,此后每次按键该表达式都会短路返回旧值,safety_briefing 永远停留在第一次输入时的内容(例如只有第一个字符),而 risk_notes 却会正常更新为完整文本。该字段会原样展示给志愿者(安全提示:{task.safety_briefing})并随 createCaseTask 持久化,属于安全提示信息错误/不完整的功能性缺陷。

As per path instructions, frontend changes should focus on data exposure and user-visible failure paths; incorrect safety-briefing text delivered to field volunteers is exactly such a failure path.

🐛 建议修复
-<TextArea required value={draft.risk_notes} maxLength={4000} rows={2} onChange={(event) => setDraft({ ...draft, risk_notes: event.target.value, safety_briefing: draft.safety_briefing || event.target.value })} fullWidth />
+<TextArea required value={draft.risk_notes} maxLength={4000} rows={2} onChange={(event) => setDraft({ ...draft, risk_notes: event.target.value, safety_briefing: event.target.value })} fullWidth />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{isCommander && detail.status !== 'closed' && <form className="mt-5 grid gap-3 border-t border-slate-200 pt-5 lg:grid-cols-2" onSubmit={(event) => { event.preventDefault(); if (!token) return; setBusy('create'); setError(''); createCaseTask(token, detail.id, draft).then(async () => { setDraft(emptyTask()); await load() }).catch((cause) => setError(messageFrom(cause))).finally(() => setBusy('')) }}><h4 className="m-0 text-sm font-semibold text-slate-950 lg:col-span-2">人工创建并分配任务</h4><Field label="关联的已确认线索" required><select required className="min-h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm" value={draft.source_clue_id} onChange={(event) => setDraft({ ...draft, source_clue_id: event.target.value })}><option value="">请选择</option>{confirmedClues.map((clue) => <option key={clue.id} value={clue.id}>{clue.content.slice(0, 80)}</option>)}</select></Field><Field label="志愿者" required><select required className="min-h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm" value={draft.volunteer_user_id} onChange={(event) => setDraft({ ...draft, volunteer_user_id: event.target.value })}><option value="">请选择</option>{volunteers.map((member) => <option key={member.user_id} value={member.user_id}>{member.display_name}</option>)}</select></Field><Field label="任务标题" required><Input required value={draft.title} maxLength={200} onChange={(event) => setDraft({ ...draft, title: event.target.value })} fullWidth /></Field><Field label="任务区域" required><Input required value={draft.area_text} maxLength={500} onChange={(event) => setDraft({ ...draft, area_text: event.target.value })} fullWidth /></Field><Field label="目标" required><TextArea required value={draft.objective} maxLength={4000} rows={2} onChange={(event) => setDraft({ ...draft, objective: event.target.value })} fullWidth /></Field><Field label="截止时间" required><Input required type="datetime-local" value={draft.due_at} onChange={(event) => setDraft({ ...draft, due_at: event.target.value ? new Date(event.target.value).toISOString() : '' })} fullWidth /></Field><Field label="背景说明" required><TextArea required value={draft.background} maxLength={10000} rows={2} onChange={(event) => setDraft({ ...draft, background: event.target.value })} fullWidth /></Field><Field label="风险说明与安全提示" required><TextArea required value={draft.risk_notes} maxLength={4000} rows={2} onChange={(event) => setDraft({ ...draft, risk_notes: event.target.value, safety_briefing: draft.safety_briefing || event.target.value })} fullWidth /></Field><Field label="预期反馈" required><TextArea required value={draft.expected_feedback} maxLength={4000} rows={2} onChange={(event) => setDraft({ ...draft, expected_feedback: event.target.value })} fullWidth /></Field><div className="flex items-end"><Button type="submit" size="sm" variant="primary" isDisabled={busy === 'create' || confirmedClues.length === 0 || volunteers.length === 0}>创建并分配</Button></div></form>}
{isCommander && detail.status !== 'closed' && <form className="mt-5 grid gap-3 border-t border-slate-200 pt-5 lg:grid-cols-2" onSubmit={(event) => { event.preventDefault(); if (!token) return; setBusy('create'); setError(''); createCaseTask(token, detail.id, draft).then(async () => { setDraft(emptyTask()); await load() }).catch((cause) => setError(messageFrom(cause))).finally(() => setBusy('')) }}><h4 className="m-0 text-sm font-semibold text-slate-950 lg:col-span-2">人工创建并分配任务</h4><Field label="关联的已确认线索" required><select required className="min-h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm" value={draft.source_clue_id} onChange={(event) => setDraft({ ...draft, source_clue_id: event.target.value })}><option value="">请选择</option>{confirmedClues.map((clue) => <option key={clue.id} value={clue.id}>{clue.content.slice(0, 80)}</option>)}</select></Field><Field label="志愿者" required><select required className="min-h-10 w-full rounded-md border border-slate-300 bg-white px-3 text-sm" value={draft.volunteer_user_id} onChange={(event) => setDraft({ ...draft, volunteer_user_id: event.target.value })}><option value="">请选择</option>{volunteers.map((member) => <option key={member.user_id} value={member.user_id}>{member.display_name}</option>)}</select></Field><Field label="任务标题" required><Input required value={draft.title} maxLength={200} onChange={(event) => setDraft({ ...draft, title: event.target.value })} fullWidth /></Field><Field label="任务区域" required><Input required value={draft.area_text} maxLength={500} onChange={(event) => setDraft({ ...draft, area_text: event.target.value })} fullWidth /></Field><Field label="目标" required><TextArea required value={draft.objective} maxLength={4000} rows={2} onChange={(event) => setDraft({ ...draft, objective: event.target.value })} fullWidth /></Field><Field label="截止时间" required><Input required type="datetime-local" value={draft.due_at} onChange={(event) => setDraft({ ...draft, due_at: event.target.value ? new Date(event.target.value).toISOString() : '' })} fullWidth /></Field><Field label="背景说明" required><TextArea required value={draft.background} maxLength={10000} rows={2} onChange={(event) => setDraft({ ...draft, background: event.target.value })} fullWidth /></Field><Field label="风险说明与安全提示" required><TextArea required value={draft.risk_notes} maxLength={4000} rows={2} onChange={(event) => setDraft({ ...draft, risk_notes: event.target.value, safety_briefing: event.target.value })} fullWidth /></Field><Field label="预期反馈" required><TextArea required value={draft.expected_feedback} maxLength={4000} rows={2} onChange={(event) => setDraft({ ...draft, expected_feedback: event.target.value })} fullWidth /></Field><div className="flex items-end"><Button type="submit" size="sm" variant="primary" isDisabled={busy === 'create' || confirmedClues.length === 0 || volunteers.length === 0}>创建并分配</Button></div></form>}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/CaseWorkspacePage.tsx` at line 788, Update the risk_notes
field handler in the manual task form so safety_briefing is assigned the current
event.target.value on every change instead of reusing the existing draft value.
Keep risk_notes synchronized with the same complete text so createCaseTask
persists and displays the full safety briefing.

Source: Path instructions

Comment on lines +1014 to +1017
<div className="rounded-md border border-slate-200 bg-white p-3">
<div className="flex items-center justify-between gap-3"><h3 className="m-0 text-base font-bold text-slate-950">经授权案件摘要</h3><Button size="sm" variant="ghost" isDisabled={!token || busy !== ''} onPress={() => void loadSummary()}>刷新摘要</Button></div>
{!summary ? <p className="mb-0 mt-2 text-sm text-slate-500">正在加载服务端确定性摘要…</p> : <><p className="mb-0 mt-2 text-xs text-slate-500">生成于 {formatDate(summary.generated_at)} · 来源范围:{summary.source_scope.join('、')}</p><div className="mt-3 grid gap-3 sm:grid-cols-2"><div><strong className="text-sm text-slate-900">最后确认信息</strong><p className="mb-0 mt-1 text-sm leading-6 text-slate-700">{summary.last_confirmed_information?.content ?? '暂无已确认信息。'}</p></div><div><strong className="text-sm text-slate-900">待核实事项</strong><p className="mb-0 mt-1 text-sm leading-6 text-slate-700">{summary.pending_verification.length === 0 ? '暂无。' : `${summary.pending_verification.length} 项仍需人工核实,未作为确认事实发布。`}</p></div></div>{summary.safety_reminders.length > 0 && <ul className="mb-0 mt-3 list-disc space-y-1 pl-5 text-xs leading-5 text-slate-600">{summary.safety_reminders.map((item) => <li key={item}>{item}</li>)}</ul>}</>}
</div>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

“经授权案件摘要”未展示 excluded_directions/task_status/current_focus

CaseSummary 类型包含 confirmed_clues、excluded_directions、current_focus、task_status 字段并已被 getCaseSummary 取回,但当前只渲染了 generated_at/source_scope/last_confirmed_information/pending_verification.length/safety_reminders。PR 目标明确要求摘要要“区分确认信息、待核实、已排除方向、任务状态与安全提醒”,目前已排除方向和任务状态在该只读摘要区完全缺失,与既定目标不符。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/CaseWorkspacePage.tsx` around lines 1014 - 1017, Update
the read-only summary JSX in CaseWorkspacePage around the summary render to
display the existing CaseSummary fields excluded_directions, task_status, and
current_focus, alongside confirmed_clues where applicable. Add clear labeled
sections distinguishing confirmed information, pending verification, excluded
directions, task status/current focus, and safety_reminders, with appropriate
empty-state text while preserving the existing generated_at and source_scope
metadata.

- expose pending active cases with minimal intake information
- allow commanders to accept unassigned cases and gain case access
- record commander acceptance in the audit log
- add commander intake API client methods and response model
- display and refresh pending intake cases in the commander workspace
- document command intake and acceptance endpoints
- allow family members to add other family members to cases

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/application/services/case_service.rs`:
- Around line 749-752: 同步更新 docs/API.md 中成员接口的接口表和授权说明,使其明确反映 CaseRole 权限:family
可添加 family 或 commander,避免继续表述为仅可邀请 commander;保留 commander
添加案件成员的现有说明,并确保相关授权矩阵与实际检查逻辑一致。
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d06008d9-7b7f-4df0-9912-c630fa42f734

📥 Commits

Reviewing files that changed from the base of the PR and between 5f3c338 and 18adfb9.

📒 Files selected for processing (6)
  • docs/API.md
  • frontend/src/api/cases.ts
  • frontend/src/pages/CaseWorkspacePage.tsx
  • src/api/models.rs
  • src/api/routes/cases.rs
  • src/application/services/case_service.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/src/api/cases.ts
  • frontend/src/pages/CaseWorkspacePage.tsx

Comment thread src/application/services/case_service.rs Outdated
- expose a minimal queue of active cases awaiting commander acceptance
- allow commander-capable accounts to accept pending cases atomically
- create commander membership and audit acceptance events on acceptance
- return conflicts for inactive or already accepted cases
- restrict family invitations to family and commander case roles
- document intake and acceptance endpoints in API and OpenAPI specs
- add frontend coverage for accepting pending commander cases
- add API tests for minimal queue visibility and duplicate acceptance
@github-actions

Copy link
Copy Markdown

Preview ready

The preview for this build is available at https://pr-151-7977c541b84a.angui.cg8.site.

Temporary demo accounts (all use the password 6a36665708c5a6993a09f012e1775682491ddbd3b3ee8bca):

Role Email
Family family@demo.invalid
Commander commander@demo.invalid
Volunteer volunteer@demo.invalid
Learner learner@demo.invalid
Administrator admin@demo.invalid

These accounts and all preview data belong only to this deployment. The next preview deployment resets the SQLite database and invalidates these credentials.

- hide family navigation from operational accounts
- redirect operational users from the family route
- add coverage for family workspace access rules

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
tests/api/case_members_post.rs (1)

103-150: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

补充接管接口的权限与状态负向覆盖。

当前仅覆盖成功和重复接管;还应验证无 commander capability 返回 403,以及非 active 案件返回 409。这两项是 accept_command_case 的服务端访问控制与状态约束,避免后续回归放宽接管条件。

As per path instructions, tests/**: Check that changed behavior has meaningful positive, negative, and boundary-case coverage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/api/case_members_post.rs` around lines 103 - 150, Extend the test
coverage around
commander_accepts_a_minimal_pending_case_without_global_case_visibility and
accept_command_case to verify that a caller without commander capability
receives 403, and that attempting takeover for a non-active case receives 409.
Keep the existing successful and duplicate-takeover assertions, and construct
the requests using the test context’s existing roles and case-state setup.

Source: Path instructions

frontend/src/pages/CaseWorkspacePage.test.tsx (1)

85-95: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

隔离新增 mock 实现,避免测试顺序依赖。

除非 Vitest 配置已全局重置 mock 实现,否则本用例留下的 listCommandIntake 和 acceptCommandCase 返回值会被后续 commander 用例继承。请在测试生命周期中重置实现,确保每个用例只使用自己的 API 响应。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/CaseWorkspacePage.test.tsx` around lines 85 - 95, Reset
mock implementations between tests in the CaseWorkspacePage test lifecycle so
the listCommandIntake and acceptCommandCase responses configured by this case do
not leak into later commander cases. Use the existing beforeEach/afterEach setup
or add per-test mock restoration, while preserving each test’s explicitly
configured API responses.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/App.test.tsx`:
- Around line 73-77: Update the test case “redirects a commander away from the
family workspace” to assert the final route pathname is “/” after rendering
“/family”. Add a pathname probe or equivalent router-location assertion, while
retaining the existing rendering and role-based checks.

In `@frontend/src/pages/CaseWorkspacePage.test.tsx`:
- Around line 85-95: 增强“lets a commander accept a minimal pending case before
viewing it”测试:在 acceptCommandCase 成功后断言案件列表或查看入口已刷新并可见,确保受理后的案件可访问;另外新增
acceptCommandCase 返回冲突错误的失败路径,断言界面展示用户可见的错误信息,并保留现有 session 与案件 ID 参数校验。

---

Nitpick comments:
In `@frontend/src/pages/CaseWorkspacePage.test.tsx`:
- Around line 85-95: Reset mock implementations between tests in the
CaseWorkspacePage test lifecycle so the listCommandIntake and acceptCommandCase
responses configured by this case do not leak into later commander cases. Use
the existing beforeEach/afterEach setup or add per-test mock restoration, while
preserving each test’s explicitly configured API responses.

In `@tests/api/case_members_post.rs`:
- Around line 103-150: Extend the test coverage around
commander_accepts_a_minimal_pending_case_without_global_case_visibility and
accept_command_case to verify that a caller without commander capability
receives 403, and that attempting takeover for a non-active case receives 409.
Keep the existing successful and duplicate-takeover assertions, and construct
the requests using the test context’s existing roles and case-state setup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f607f7a7-dbf7-40e6-9724-acf1d49a92ef

📥 Commits

Reviewing files that changed from the base of the PR and between 18adfb9 and d08467d.

📒 Files selected for processing (9)
  • docs/API.md
  • docs/openapi.yaml
  • frontend/src/App.test.tsx
  • frontend/src/App.tsx
  • frontend/src/components/AppShell.tsx
  • frontend/src/pages/CaseWorkspacePage.test.tsx
  • src/api/routes/cases.rs
  • src/application/services/case_service.rs
  • tests/api/case_members_post.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • docs/API.md
  • src/api/routes/cases.rs
  • src/application/services/case_service.rs

Comment thread frontend/src/App.test.tsx
Comment on lines +73 to +77
it('redirects a commander away from the family workspace', async () => {
setAuth('member', ['commander'])
renderApp('/family')
expect(await screen.findByText('行动总览')).toBeInTheDocument()
expect(screen.queryByRole('link', { name: '家属端' })).not.toBeInTheDocument()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

直接断言重定向目标,避免测试误判。

当前断言没有检查最终路径;“家属端”导航缺失对 commander 本来就成立,不能证明 /family 已重定向到 /。请增加 pathname 探针或等价的路由位置断言。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/App.test.tsx` around lines 73 - 77, Update the test case
“redirects a commander away from the family workspace” to assert the final route
pathname is “/” after rendering “/family”. Add a pathname probe or equivalent
router-location assertion, while retaining the existing rendering and role-based
checks.

Comment on lines +85 to +95
it('lets a commander accept a minimal pending case before viewing it', async () => {
mocked.listCases.mockResolvedValue([])
mocked.listCommandIntake.mockResolvedValue([{ id: 'pending-case', case_code: 'AG-PENDING', created_at: '2026-07-24T00:00:00Z', last_seen_at: null, area_hint: 'Fictional north gate' }])
mocked.acceptCommandCase.mockResolvedValue(detail('pending-case', 'Accepted case', 'commander'))

render(<CaseWorkspacePage mode="commander" />)

expect(await screen.findByText('AG-PENDING')).toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: '受理案件' }))
await waitFor(() => expect(mocked.acceptCommandCase).toHaveBeenCalledWith('test-session', 'pending-case'))
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

补上受理后的状态与失败路径断言。

当前用例只验证 API 参数;即使受理后案件列表没有刷新、案件不可查看,或冲突错误被静默处理,测试仍会通过。请断言受理后的案件可见/队列已刷新,并覆盖 acceptCommandCase 返回冲突时的用户可见错误。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/CaseWorkspacePage.test.tsx` around lines 85 - 95, 增强“lets
a commander accept a minimal pending case before viewing it”测试:在
acceptCommandCase 成功后断言案件列表或查看入口已刷新并可见,确保受理后的案件可访问;另外新增 acceptCommandCase
返回冲突错误的失败路径,断言界面展示用户可见的错误信息,并保留现有 session 与案件 ID 参数校验。

@github-actions

Copy link
Copy Markdown

Preview ready

The preview for this build is available at https://pr-151-d08467dccc28.angui.cg8.site.

Temporary demo accounts (all use the password 9fcad5c855d07a151a54c0a274b5033332271168a7ce1357):

Role Email
Family family@demo.invalid
Commander commander@demo.invalid
Volunteer volunteer@demo.invalid
Learner learner@demo.invalid
Administrator admin@demo.invalid

These accounts and all preview data belong only to this deployment. The next preview deployment resets the SQLite database and invalidates these credentials.

- let family members invite family members or commanders
- keep volunteer invitations restricted to commanders
- submit the selected permitted role when adding members
- add coverage for family member invitation role options

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
frontend/src/pages/CaseWorkspacePage.test.tsx (2)

239-253: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

同时验证 commander 角色确实会被提交。

测试只选择 family 并断言一次;即使实现始终传入 family,当前测试也会通过。请再选择 commander、提交另一邮箱,并断言 addCaseMember 的第四个参数为 'commander'。

As per path instructions,这能直接验证前端提交值与后端角色契约一致。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/CaseWorkspacePage.test.tsx` around lines 239 - 253, Extend
the test around the roleSelect and addCaseMember interactions to also select the
commander option, submit a different member email, and assert
mocked.addCaseMember is called with 'commander' as the fourth argument. Preserve
the existing family-role assertion while adding coverage that the selected
commander value is submitted correctly.

Source: Path instructions


101-110: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

补充地图元数据断言。

当前只断言地点名称、文字地址和“仅文字地点”。即使页面丢失 location_precision、source 或 review_status 的展示,测试仍会通过。请至少为精度、来源和审核状态增加用户可见断言,避免地图信息契约回归。

As per path instructions,前端测试应覆盖 API 契约与用户可见的数据暴露。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/CaseWorkspacePage.test.tsx` around lines 101 - 110, 补充
CaseWorkspacePage 测试中地图地点元数据的用户可见断言:在现有 “Fictional market”“North gate, fictional
park” 和“仅文字地点”断言旁,验证 location_precision、source 与 review_status
对应的展示文本均存在。使用页面实际渲染的标签或文案,确保 API 返回的精度、来源和审核状态都被用户看到。

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@frontend/src/pages/CaseWorkspacePage.test.tsx`:
- Around line 239-253: Extend the test around the roleSelect and addCaseMember
interactions to also select the commander option, submit a different member
email, and assert mocked.addCaseMember is called with 'commander' as the fourth
argument. Preserve the existing family-role assertion while adding coverage that
the selected commander value is submitted correctly.
- Around line 101-110: 补充 CaseWorkspacePage 测试中地图地点元数据的用户可见断言:在现有 “Fictional
market”“North gate, fictional park” 和“仅文字地点”断言旁,验证 location_precision、source 与
review_status 对应的展示文本均存在。使用页面实际渲染的标签或文案,确保 API 返回的精度、来源和审核状态都被用户看到。

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a83efc15-34be-4822-a92f-e5cf02f811b9

📥 Commits

Reviewing files that changed from the base of the PR and between d08467d and f417060.

📒 Files selected for processing (2)
  • frontend/src/pages/CaseWorkspacePage.test.tsx
  • frontend/src/pages/CaseWorkspacePage.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/pages/CaseWorkspacePage.tsx

@github-actions

Copy link
Copy Markdown

Preview ready

The preview for this build is available at https://pr-151-f417060f9fa5.angui.cg8.site.

Temporary demo accounts (all use the password 17c8c1f5327cbdb19ab31b3d02a21354d83abd8cbcaf01e9):

Role Email
Family family@demo.invalid
Commander commander@demo.invalid
Volunteer volunteer@demo.invalid
Learner learner@demo.invalid
Administrator admin@demo.invalid

These accounts and all preview data belong only to this deployment. The next preview deployment resets the SQLite database and invalidates these credentials.

- Include elder age in command intake API responses and schema
- Preserve text area hints while exposing last-seen timestamps
- Display pending intake details in commander workspace cards
- Add commander intake metrics and status rows to the dashboard
- Update API documentation, types, and intake acceptance tests

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
frontend/src/pages/DashboardPage.tsx (1)

30-55: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

隔离待受理队列的失败,避免清空整个总览。

listCommandIntake() 一旦拒绝,Promise.all 会进入 catch 并清空普通案件;因此指挥官即使仍有可访问案件,也只能看到全页加载失败。将待受理请求降级为 [] 并保留警告,只有主案件请求失败时才清空总览;同时覆盖“待受理请求失败但普通案件仍展示”的回归场景。

建议修复
-      const [items, intakeCases] = await Promise.all([
+      const [items, intakeResult] = await Promise.all([
         listCases(token),
-        isCommander ? listCommandIntake(token) : Promise.resolve([]),
+        isCommander
+          ? listCommandIntake(token).then(
+              (cases) => ({ cases, failed: false }),
+              () => ({ cases: [], failed: true }),
+            )
+          : Promise.resolve({ cases: [], failed: false }),
       ])
+      const intakeCases = intakeResult.cases
@@
-      if (loaded.length !== items.length) {
-        setError('部分案件详情暂时不可用,统计数据可能不完整。')
+      if (loaded.length !== items.length || intakeResult.failed) {
+        setError('部分数据暂时不可用,统计数据可能不完整。')
       }

As per path instructions, frontend/** 要关注“authentication state, validation, data exposure, and user-visible failure paths”。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/DashboardPage.tsx` around lines 30 - 55, Separate the
listCommandIntake request from the Promise.all that loads ordinary cases so its
rejection falls back to an empty intakeCases list and records a user-visible
warning. Keep successful ordinary case loading and details rendering intact when
intake loading fails; only the primary listCases failure should clear the
overview and enter the existing fatal error path. Add or update regression
coverage for a failed intake request with successfully displayed ordinary cases.

Source: Path instructions

src/application/services/case_service.rs (1)

182-201: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

加数据库级约束,避免并发重复接管。
这里还是“先查再插入”的竞态;case_memberships 目前只有 (case_id, user_id) 唯一约束,挡不住两个不同用户同时以 commander 身份接管同一案件。请为“每案仅允许一个 commander”补上数据库级约束,并把唯一冲突统一映射成 409 Conflict,否则并发下要么落成重复接管,要么抛出数据库错误。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/application/services/case_service.rs` around lines 182 - 201, 为
case_memberships 增加数据库级的“每个 case_id 只能有一个
commander”唯一约束或等效部分唯一索引,避免并发接管产生重复记录;同时更新接管流程及 insert_membership
的错误处理,将该唯一约束冲突统一转换为 ApiError::Conflict,保留现有成功路径和重复接管提示语义。

Source: Path instructions

🧹 Nitpick comments (3)
src/application/services/case_service.rs (1)

902-929: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

避免成员列表的串行 N+1 查询。

当前每个成员至少执行一次用户查询和一次全局能力查询,并且全部串行等待;成员数量增大时会显著放大数据库负载和接口延迟。请批量加载用户及能力,或增加分页并并行/批量组装响应。

As per path instructions: src/** 应重点关注可靠性和可扩展性风险。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/application/services/case_service.rs` around lines 902 - 929, Update
list_case_members to eliminate the per-membership serial user and
global-capabilities queries: batch-load all referenced users and their
capabilities, then assemble responses from those results while preserving the
existing ordering and error behavior. Use the membership user IDs and existing
capability-loading symbols rather than issuing queries inside the loop.

Source: Path instructions

frontend/src/pages/CaseWorkspacePage.test.tsx (2)

87-92: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

为新增的 elder_age 补充渲染断言。

Fixture 已加入 elder_age: 76,但测试没有验证该字段在受理卡片中可见,因此年龄字段即使被页面忽略,测试仍会通过。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/CaseWorkspacePage.test.tsx` around lines 87 - 92, Update
the CaseWorkspacePage test around the listCommandIntake fixture and rendered
acceptance card to assert that the elder_age value 76 is visible. Keep the
existing AG-PENDING assertion and verify the age field is rendered on the card.

221-255: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

补上指挥角色的正向提交路径。

该用例只实际提交了 family,尚未验证选择“指挥”后 addCaseMember 收到 commander,因此测试名描述的另一条授权路径仍可能回归。

建议补充
     await waitFor(() => expect(mocked.addCaseMember).toHaveBeenCalledWith(
       'test-session',
       'case-family',
       'family-member@demo.invalid',
       'family',
     ))
+
+    fireEvent.change(roleSelect, { target: { value: 'commander' } })
+    fireEvent.change(screen.getByPlaceholderText('成员邮箱'), {
+      target: { value: 'commander@demo.invalid' },
+    })
+    fireEvent.click(screen.getByRole('button', { name: '添加案件成员' }))
+    await waitFor(() => expect(mocked.addCaseMember).toHaveBeenCalledWith(
+      'test-session',
+      'case-family',
+      'commander@demo.invalid',
+      'commander',
+    ))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/CaseWorkspacePage.test.tsx` around lines 221 - 255, Extend
the test case around the existing roleSelect and addCaseMember assertions to
also select the “指挥” option, enter a commander email, and submit the form.
Verify addCaseMember is called with the same session and case identifiers, the
commander email, and the role value “commander”, while preserving the existing
family-member submission coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/application/services/case_service.rs`:
- Around line 151-154: Update the case mapping around area_hint and elder_age so
the pending-case queue never exposes the raw profile.last_seen_location text.
Use an existing coarse-grained region field, or apply irreversible server-side
regionalization before assigning area_hint, and add a test verifying precise
location data is absent from queue responses.

---

Outside diff comments:
In `@frontend/src/pages/DashboardPage.tsx`:
- Around line 30-55: Separate the listCommandIntake request from the Promise.all
that loads ordinary cases so its rejection falls back to an empty intakeCases
list and records a user-visible warning. Keep successful ordinary case loading
and details rendering intact when intake loading fails; only the primary
listCases failure should clear the overview and enter the existing fatal error
path. Add or update regression coverage for a failed intake request with
successfully displayed ordinary cases.

In `@src/application/services/case_service.rs`:
- Around line 182-201: 为 case_memberships 增加数据库级的“每个 case_id 只能有一个
commander”唯一约束或等效部分唯一索引,避免并发接管产生重复记录;同时更新接管流程及 insert_membership
的错误处理,将该唯一约束冲突统一转换为 ApiError::Conflict,保留现有成功路径和重复接管提示语义。

---

Nitpick comments:
In `@frontend/src/pages/CaseWorkspacePage.test.tsx`:
- Around line 87-92: Update the CaseWorkspacePage test around the
listCommandIntake fixture and rendered acceptance card to assert that the
elder_age value 76 is visible. Keep the existing AG-PENDING assertion and verify
the age field is rendered on the card.
- Around line 221-255: Extend the test case around the existing roleSelect and
addCaseMember assertions to also select the “指挥” option, enter a commander
email, and submit the form. Verify addCaseMember is called with the same session
and case identifiers, the commander email, and the role value “commander”, while
preserving the existing family-member submission coverage.

In `@src/application/services/case_service.rs`:
- Around line 902-929: Update list_case_members to eliminate the per-membership
serial user and global-capabilities queries: batch-load all referenced users and
their capabilities, then assemble responses from those results while preserving
the existing ordering and error behavior. Use the membership user IDs and
existing capability-loading symbols rather than issuing queries inside the loop.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d71175ea-b374-44e6-a6d6-642f0a4dedf2

📥 Commits

Reviewing files that changed from the base of the PR and between f417060 and 021ed10.

📒 Files selected for processing (10)
  • docs/API.md
  • docs/openapi.yaml
  • frontend/src/api/cases.ts
  • frontend/src/pages/CaseWorkspacePage.test.tsx
  • frontend/src/pages/CaseWorkspacePage.tsx
  • frontend/src/pages/DashboardPage.test.tsx
  • frontend/src/pages/DashboardPage.tsx
  • src/api/models.rs
  • src/application/services/case_service.rs
  • tests/api/case_members_post.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/API.md
  • frontend/src/pages/CaseWorkspacePage.tsx

Comment on lines +151 to +154
area_hint: profile
.as_ref()
.and_then(|item| item.last_seen_location.clone()),
elder_age: profile.and_then(|item| item.age),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

不要直接把原始最后出现地点作为待受理队列的 area_hint。

last_seen_location 是自由文本,可能包含精确地址;当前会在案件尚未被接管前直接返回给所有具备 commander 能力的账号,与接口声明“不暴露精确坐标/敏感案情”的隐私边界不一致。请改用单独的粗粒度区域字段,或在服务端进行不可逆的区域化处理,并补充精确地点不会出现在队列响应中的测试。

As per path instructions: src/** 应重点检查隐私与数据暴露风险。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/application/services/case_service.rs` around lines 151 - 154, Update the
case mapping around area_hint and elder_age so the pending-case queue never
exposes the raw profile.last_seen_location text. Use an existing coarse-grained
region field, or apply irreversible server-side regionalization before assigning
area_hint, and add a test verifying precise location data is absent from queue
responses.

Source: Path instructions

@github-actions

Copy link
Copy Markdown

Preview ready

The preview for this build is available at https://pr-151-021ed1003572.angui.cg8.site.

Temporary demo accounts (all use the password f5334b83e9efaacf4dbba403c4af43a91f794a78b4c7e5ba):

Role Email
Family family@demo.invalid
Commander commander@demo.invalid
Volunteer volunteer@demo.invalid
Learner learner@demo.invalid
Administrator admin@demo.invalid

These accounts and all preview data belong only to this deployment. The next preview deployment resets the SQLite database and invalidates these credentials.

- create a non-blocking, source-scoped summary draft after commander clue review
- add commander-only endpoint to retrieve the latest pending review draft
- update the workspace to load and display automatically generated drafts
- remove manual draft generation from the standard review workflow
- label deterministic fallback drafts without attributing them to an AI model
- keep draft generation failures from blocking completed clue reviews
- document automatic generation, fallback behavior, and internal regeneration use
- add API coverage for retrieving the generated pending summary draft

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/application/services/case_collaboration_service.rs (1)

454-497: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

创建新自动草稿时关闭旧的待审核草稿。

每次审核都会插入新的 pending_review 记录,但读取接口只返回最新一条;较早草稿会永久停留在待审核状态且无法被工作区审核。请在同一事务内将同案件旧 pending 草稿标记为非待审核状态,或改为更新现有 pending 草稿,并添加多次线索审核的覆盖。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/application/services/case_collaboration_service.rs` around lines 454 -
497, Update the draft creation flow around the summary_drafts ActiveModel
insertion so that, within the same transaction, any existing pending_review
draft for the case is transitioned to a non-pending status before inserting the
new draft. Preserve the new draft’s initial status and audit behavior, and add
coverage for repeated clue reviews to verify older pending drafts are no longer
left reviewable.
frontend/src/pages/CaseWorkspacePage.test.tsx (1)

6-44: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

补齐任务和摘要 API 的 mock 与默认成功响应。

渲染 commander 工作区会调用 listCaseTasks、listCaseMembers 和 getCaseSummary,但 mock 模块未导出它们;测试没有覆盖新增看板和摘要的正常加载路径,可能只是在吞掉运行时错误。请导出这些 mock,并为任务分页、成员列表、摘要和 getLatestSummaryDraft 设置有效默认值。

As per path instructions, “tests/**: Check that changed behavior has meaningful positive, negative, and boundary-case coverage.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/CaseWorkspacePage.test.tsx` around lines 6 - 44, 补齐
CaseWorkspacePage 测试中的 API mock:在 mocked 状态及 ../api/cases mock 工厂中导出
listCaseTasks、listCaseMembers 和 getCaseSummary,并为 listCaseTasks 分页、成员列表、摘要及
getLatestSummaryDraft 配置有效的默认成功响应。更新 commander
工作区测试,覆盖看板和摘要正常加载,同时补充相关失败与边界场景,确保不是吞掉运行时错误。

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/openapi.yaml`:
- Around line 982-995: 补充 getLatestSummaryDraft 端点的响应契约,加入引用现有 Forbidden 响应定义的
403 状态码,保留当前 401、404 和 500 响应不变。

In `@src/api/routes/clues.rs`:
- Around line 26-35: 移除审核请求中对
crate::services::case_collaboration_service::create_summary_draft 的同步 await
调用;改为在审核自身事务成功提交后,将摘要生成任务写入可靠的后台队列或 outbox,并由异步 worker
执行该操作,使审核接口不等待摘要查询、写入及审计事务。

In `@tests/api/clue_review_patch.rs`:
- Around line 48-59: 扩展审核前的测试覆盖:在成功获取待审核草稿之前,使用 commander 身份断言无草稿时端点返回 JSON
null,并使用家属 token 请求同一 case-restricted 端点且断言返回 403;保留现有 commander 成功路径及其字段断言。

---

Outside diff comments:
In `@frontend/src/pages/CaseWorkspacePage.test.tsx`:
- Around line 6-44: 补齐 CaseWorkspacePage 测试中的 API mock:在 mocked 状态及 ../api/cases
mock 工厂中导出 listCaseTasks、listCaseMembers 和 getCaseSummary,并为 listCaseTasks
分页、成员列表、摘要及 getLatestSummaryDraft 配置有效的默认成功响应。更新 commander
工作区测试,覆盖看板和摘要正常加载,同时补充相关失败与边界场景,确保不是吞掉运行时错误。

In `@src/application/services/case_collaboration_service.rs`:
- Around line 454-497: Update the draft creation flow around the summary_drafts
ActiveModel insertion so that, within the same transaction, any existing
pending_review draft for the case is transitioned to a non-pending status before
inserting the new draft. Preserve the new draft’s initial status and audit
behavior, and add coverage for repeated clue reviews to verify older pending
drafts are no longer left reviewable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 46ce09ba-9f6d-49f9-9ee8-40f99771499b

📥 Commits

Reviewing files that changed from the base of the PR and between 021ed10 and bf250fb.

📒 Files selected for processing (9)
  • docs/API.md
  • docs/openapi.yaml
  • frontend/src/api/cases.ts
  • frontend/src/pages/CaseWorkspacePage.test.tsx
  • frontend/src/pages/CaseWorkspacePage.tsx
  • src/api/routes/cases.rs
  • src/api/routes/clues.rs
  • src/application/services/case_collaboration_service.rs
  • tests/api/clue_review_patch.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/API.md

Comment thread docs/openapi.yaml
Comment on lines +982 to +995
get:
tags: [Cases]
operationId: getLatestSummaryDraft
summary: Get the latest pending automated summary draft
description: Commander only. A pending-review draft is created automatically after a commander completes a clue review. The response is null when there is no draft awaiting review. A deterministic, source-scoped fallback is used when AI generation is unavailable, and it is still subject to the same human review before publication.
security: [{ bearerAuth: [] }]
x-account-types: [member]
x-case-roles: [commander]
x-data-classification: case-restricted
responses:
"200": { description: Latest pending-review summary draft or null, content: { application/json: { schema: { oneOf: [{ $ref: "#/components/schemas/SummaryDraft" }, { type: "null" }] } } } }
"401": { $ref: "#/components/responses/Unauthorized" }
"404": { $ref: "#/components/responses/NotFound" }
"500": { $ref: "#/components/responses/InternalError" }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

补充 403 Forbidden 响应契约。

该端点要求 commander 角色,服务层会返回 403;当前文档只声明 401、404、500,生成客户端无法可靠处理正常的越权结果。

       responses:
         "200": { description: Latest pending-review summary draft or null, content: { application/json: { schema: { oneOf: [{ $ref: "`#/components/schemas/SummaryDraft`" }, { type: "null" }] } } } }
         "401": { $ref: "`#/components/responses/Unauthorized`" }
+        "403": { $ref: "`#/components/responses/Forbidden`" }
         "404": { $ref: "`#/components/responses/NotFound`" }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
get:
tags: [Cases]
operationId: getLatestSummaryDraft
summary: Get the latest pending automated summary draft
description: Commander only. A pending-review draft is created automatically after a commander completes a clue review. The response is null when there is no draft awaiting review. A deterministic, source-scoped fallback is used when AI generation is unavailable, and it is still subject to the same human review before publication.
security: [{ bearerAuth: [] }]
x-account-types: [member]
x-case-roles: [commander]
x-data-classification: case-restricted
responses:
"200": { description: Latest pending-review summary draft or null, content: { application/json: { schema: { oneOf: [{ $ref: "#/components/schemas/SummaryDraft" }, { type: "null" }] } } } }
"401": { $ref: "#/components/responses/Unauthorized" }
"404": { $ref: "#/components/responses/NotFound" }
"500": { $ref: "#/components/responses/InternalError" }
get:
tags: [Cases]
operationId: getLatestSummaryDraft
summary: Get the latest pending automated summary draft
description: Commander only. A pending-review draft is created automatically after a commander completes a clue review. The response is null when there is no draft awaiting review. A deterministic, source-scoped fallback is used when AI generation is unavailable, and it is still subject to the same human review before publication.
security: [{ bearerAuth: [] }]
x-account-types: [member]
x-case-roles: [commander]
x-data-classification: case-restricted
responses:
"200": { description: Latest pending-review summary draft or null, content: { application/json: { schema: { oneOf: [{ $ref: "`#/components/schemas/SummaryDraft`" }, { type: "null" }] } } } }
"401": { $ref: "`#/components/responses/Unauthorized`" }
"403": { $ref: "`#/components/responses/Forbidden`" }
"404": { $ref: "`#/components/responses/NotFound`" }
"500": { $ref: "`#/components/responses/InternalError`" }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/openapi.yaml` around lines 982 - 995, 补充 getLatestSummaryDraft
端点的响应契约,加入引用现有 Forbidden 响应定义的 403 状态码,保留当前 401、404 和 500 响应不变。

Comment thread src/api/routes/clues.rs
Comment on lines +26 to +35
// Summary generation is a non-blocking derivative of a completed human review.
// Its controlled rule-based fallback must never prevent the review itself from persisting.
let _ = crate::services::case_collaboration_service::create_summary_draft(
&state.db,
&auth,
&clue.case_id,
crate::models::CreateSummaryDraftRequest { content: None },
&state.ai_gateway,
)
.await;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

不要在审核请求中同步等待摘要生成。

let _ = ... .await 只忽略错误,仍会让审核响应等待摘要查询、写入和审计事务;数据库拥塞时会拖慢甚至挂住审核接口。将此派生操作写入可靠后台队列/outbox,并让审核提交在自身事务完成后立即返回。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/routes/clues.rs` around lines 26 - 35, 移除审核请求中对
crate::services::case_collaboration_service::create_summary_draft 的同步 await
调用;改为在审核自身事务成功提交后,将摘要生成任务写入可靠的后台队列或 outbox,并由异步 worker
执行该操作,使审核接口不等待摘要查询、写入及审计事务。

Comment on lines +48 to +59
let pending_draft = test::call_service(
&app,
test::TestRequest::get()
.uri(&format!("/api/cases/{case_id}/summary-drafts"))
.insert_header((header::AUTHORIZATION, format!("Bearer {commander_token}")))
.to_request(),
)
.await;
assert_eq!(pending_draft.status(), StatusCode::OK);
let pending_draft: Value = test::read_body_json(pending_draft).await;
assert_eq!(pending_draft["status"], "pending_review");
assert_eq!(pending_draft["publication_eligible"], true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

覆盖无草稿和非 commander 访问。

除当前成功路径外,请在审核前断言 commander 得到 JSON null,并用家属 token 断言该 case-restricted 端点返回 403。这能锁定 Option 响应和 commander 授权边界。

As per path instructions, “tests/**: Check that changed behavior has meaningful positive, negative, and boundary-case coverage.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/api/clue_review_patch.rs` around lines 48 - 59,
扩展审核前的测试覆盖:在成功获取待审核草稿之前,使用 commander 身份断言无草稿时端点返回 JSON null,并使用家属 token 请求同一
case-restricted 端点且断言返回 403;保留现有 commander 成功路径及其字段断言。

Source: Path instructions

@github-actions

Copy link
Copy Markdown

Preview ready

The preview for this build is available at https://pr-151-bf250fbe7883.angui.cg8.site.

Temporary demo accounts (all use the password 670ce792bde06aa8982e93ebbe73c78ae77bdacad1a0a9a3):

Role Email
Family family@demo.invalid
Commander commander@demo.invalid
Volunteer volunteer@demo.invalid
Learner learner@demo.invalid
Administrator admin@demo.invalid

These accounts and all preview data belong only to this deployment. The next preview deployment resets the SQLite database and invalidates these credentials.

- Reject clue creation for resolved and closed cases in the case service
- Hide the clue submission form when a case is not active
- Add frontend coverage for resolved-case clue submission visibility
- Extend API tests to reject clue creation on resolved cases
- Update API and OpenAPI conflict documentation and error messages

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
frontend/src/pages/CaseWorkspacePage.test.tsx (1)

27-44: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

补齐显式 API mock,否则详情页测试会直接抛错。

CaseWorkspacePage 已调用 listCaseTasks、listCaseMembers、createCaseTask、updateTaskStatus、submitTaskFeedback、getCaseSummary 和 reviewSummaryDraft,但显式 mock 工厂没有导出这些函数;TaskBoard 挂载时调用 listCaseTasks 会得到 undefined。

请为新增 API 提供 vi.fn() 及工厂透传实现,并为相关测试设置默认返回值。

As per path instructions: tests/** 应检查变更行为的有效测试覆盖。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/CaseWorkspacePage.test.tsx` around lines 27 - 44, 补齐
CaseWorkspacePage.test.tsx 中 mock 工厂对
listCaseTasks、listCaseMembers、createCaseTask、updateTaskStatus、submitTaskFeedback、getCaseSummary
和 reviewSummaryDraft 的显式导出:查询 API 使用 mocked 透传包装,写入 API 提供 vi.fn()。在相关测试的默认 mock
设置中为这些 API 配置符合页面渲染预期的返回值,确保 TaskBoard 及摘要流程不会因 undefined 直接抛错,并补充或更新 tests/**
下覆盖这些新增行为的有效测试。

Source: Path instructions

src/application/services/case_service.rs (1)

182-201: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

补上受理的数据库级原子保护
case_memberships 只有 (case_id, user_id) 唯一约束,挡不住两个不同 commander 同时受理同一案件;这里仍需要把“每个 case 仅一个 commander”的约束下沉到数据库,或改成原子插入/冲突处理,并补并发测试。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/application/services/case_service.rs` around lines 182 - 201, 在受理流程中更新
commander 成员创建逻辑,不能依赖 `case_memberships::Entity::find` 检查后再调用
`insert_membership`;改用数据库级唯一约束或原子插入并处理冲突,确保每个 case 只能存在一个
commander。同步补充并发受理测试,验证多个不同用户同时操作时仅一个成功且其余返回冲突。

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@frontend/src/pages/CaseWorkspacePage.test.tsx`:
- Around line 27-44: 补齐 CaseWorkspacePage.test.tsx 中 mock 工厂对
listCaseTasks、listCaseMembers、createCaseTask、updateTaskStatus、submitTaskFeedback、getCaseSummary
和 reviewSummaryDraft 的显式导出:查询 API 使用 mocked 透传包装,写入 API 提供 vi.fn()。在相关测试的默认 mock
设置中为这些 API 配置符合页面渲染预期的返回值,确保 TaskBoard 及摘要流程不会因 undefined 直接抛错,并补充或更新 tests/**
下覆盖这些新增行为的有效测试。

In `@src/application/services/case_service.rs`:
- Around line 182-201: 在受理流程中更新 commander 成员创建逻辑,不能依赖
`case_memberships::Entity::find` 检查后再调用
`insert_membership`;改用数据库级唯一约束或原子插入并处理冲突,确保每个 case 只能存在一个
commander。同步补充并发受理测试,验证多个不同用户同时操作时仅一个成功且其余返回冲突。

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 51f532cf-566a-4a0d-be27-c3045edc762d

📥 Commits

Reviewing files that changed from the base of the PR and between bf250fb and 6fc13cd.

📒 Files selected for processing (6)
  • docs/API.md
  • docs/openapi.yaml
  • frontend/src/pages/CaseWorkspacePage.test.tsx
  • frontend/src/pages/CaseWorkspacePage.tsx
  • src/application/services/case_service.rs
  • tests/api/case_clues_post.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/openapi.yaml

@github-actions

Copy link
Copy Markdown

Preview ready

The preview for this build is available at https://pr-151-6fc13cd271af.angui.cg8.site.

Temporary demo accounts (all use the password f39d607d2019d0e6fab7f0b6a4d7868ad4aff6ef0ac1521d):

Role Email
Family family@demo.invalid
Commander commander@demo.invalid
Volunteer volunteer@demo.invalid
Learner learner@demo.invalid
Administrator admin@demo.invalid

These accounts and all preview data belong only to this deployment. The next preview deployment resets the SQLite database and invalidates these credentials.

@cg8-5712
cg8-5712 merged commit 33a0cd5 into main Jul 30, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants