修复已取消/不可响应的会议列表卡片仍提供拒绝/接受按钮,点击后端403

MeetingCard对拒绝/接受按钮的渲染没有像MeetingDetail那样按
permissions.can_respond门禁,取消后的会议(my_response仍是
pending,属正常状态)会一直显示看似可用的按钮,点击后PUT
.../my-response正确返回403 PERMISSION_DENIED,用户点了却没反应。

第十二轮最小粒度测试 Ch9 发现,用真实取消会议(mtg_0w2TnfQiqaMwy8Bj)
复现(该agent进行中因会话限流中断,此项修复在中断前已完成,事后
确认无误后提交)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-06 04:23:48 +08:00
co-authored by Claude Sonnet 5
parent 164a41270c
commit 22468edf6b
+13 -2
View File
@@ -302,10 +302,21 @@ function MeetingCard({meeting, myUserId, onRespond, onToast}) {
{!isOrganizer ? (
meeting.my_response && meeting.my_response !== 'pending' ?
<footer><button type="button" className="meeting-responded" disabled>{RESPONSE_LABEL[meeting.my_response] || meeting.my_response}</button></footer> :
<footer className="meeting-response">
// Live-repro'd (round 12): this branch used to render regardless of `permissions.can_respond`,
// so a canceled meeting the viewer never responded to (my_response still 'pending', a normal
// state for a canceled invite -- canceling doesn't force every invitee to respond first) kept
// showing live-looking 拒绝/接受 buttons here. Clicking either got a 403 PERMISSION_DENIED from
// PUT .../my-response (confirmed against a real canceled meeting, mtg_0w2TnfQiqaMwy8Bj) --
// the request body/version were fine, the backend just correctly refuses to accept a response
// once a meeting can no longer be responded to. MeetingDetail already gates its own copy of
// these same two buttons on this exact flag (line ~406 below); this list card independently
// duplicates the buttons but had never adopted the same guard. Requirements 9.4 also calls for
// expired meetings to become read-only history, which this same flag should cover -- can_respond
// is a general "still actionable" signal, not canceled-specific.
(meeting.permissions && meeting.permissions.can_respond ? <footer className="meeting-response">
<button type="button" disabled={busy} onClick={() => respond('declined')}>拒绝</button>
<button type="button" className="primary" disabled={busy} onClick={() => respond('accepted')}>接受</button>
</footer>
</footer> : null)
) : null}
</article>;
}