diff --git a/.trellis/spec/hyb-oa/frontend-react-vite-design.md b/.trellis/spec/hyb-oa/frontend-react-vite-design.md
index 4dd0ad3f..126f085a 100644
--- a/.trellis/spec/hyb-oa/frontend-react-vite-design.md
+++ b/.trellis/spec/hyb-oa/frontend-react-vite-design.md
@@ -2712,3 +2712,70 @@ const MANAGEMENT_ACTIVE_PATHS = ["/people", "/system/users"];
isManagementPath(pathname, permissions);
...
```
+
+## Scenario: Authentication Password Input Sequential Focus
+
+### 1. Scope / Trigger
+
+- Trigger: changing the shared password input or any login, password-change or password-recovery form.
+- Purpose: preserve the domestic continuous-form Tab flow without letting the inline visibility control create an extra sequential stop.
+
+### 2. Signatures
+
+- Shared component: `client/src/components/ui/password-input.tsx`.
+- Visibility control: native `button type="button"` with `tabIndex={-1}`, dynamic `aria-label`, boolean `aria-pressed` and `aria-controls=`.
+- Sequential focus follows DOM order; do not use positive `tabIndex` values.
+
+### 3. Contracts
+
+- Pressing Tab from a password input skips its inline visibility button and reaches the next sequential form control in the DOM.
+- Existing links such as `忘记密码?` remain keyboard reachable; do not remove unrelated controls merely to make a password-to-submit shortcut.
+- Mouse and touch can still toggle visibility. The label changes between `显示密码` and `隐藏密码`, pressed state follows visibility, and the input value is preserved.
+- Login, password-change and password-recovery pages reuse the shared component and therefore keep one focus contract.
+- The focus change does not alter password validation, autofill, form submission, authentication APIs, email delivery or server configuration.
+
+### 4. Validation & Error Matrix
+
+- Tab lands on the inline eye button -> sequential-focus regression.
+- Tab skips the next real input, link or submit control -> DOM-order regression; do not repair with a positive `tabIndex`.
+- Visibility click changes type but clears the value -> input-state regression.
+- Toggle lacks its dynamic label, pressed state or input association -> accessibility regression.
+- Enter from the confirmation password input does not submit the valid form -> form-keyboard regression.
+
+### 5. Good/Base/Bad Cases
+
+- Good: reset form moves `新密码 -> 确认新密码 -> 重置密码`, while either eye control remains clickable and preserves the password.
+- Base: login password moves to the existing `忘记密码?` link because that link is the next DOM control, then continues to login submit.
+- Bad: keep the eye button in sequential Tab despite the approved form contract, remove all button semantics, or assign positive indices to force an artificial order.
+
+### 6. Tests Required
+
+- Playwright asserts the login password reaches the forgot-password link, password-change fields traverse old/new/confirm/submit, and recovery fields traverse new/confirm/submit.
+- Assert `tabindex=-1`, `aria-controls`, dynamic `aria-label`, `aria-pressed`, click-to-toggle and value preservation.
+- Assert Enter from the valid confirmation-password field submits the form.
+- Run `cd client && npm run typecheck && npm run build`, focused authentication E2E, Trellis/process/sensitive gates and `git diff --check`.
+
+### 7. Wrong vs Correct
+
+#### Wrong
+
+```tsx
+
+```
+
+#### Correct
+
+```tsx
+
+```
diff --git a/.trellis/tasks/07-28-all-account-login-readiness/design.md b/.trellis/tasks/07-28-all-account-login-readiness/design.md
index bc69b4db..a9fad3ce 100644
--- a/.trellis/tasks/07-28-all-account-login-readiness/design.md
+++ b/.trellis/tasks/07-28-all-account-login-readiness/design.md
@@ -19,10 +19,10 @@
## 2. Login Baseline
-每个 active 账户必须同时满足:
+每个 active 账户必须具备以下两类能力,实际真人覆盖另行记录:
1. `WeCom SSO ready`:属于当前企微通讯录的员工存在唯一 binding,并由本人完成一次工作台直接登录;首次登录不需要 OA 密码。
-2. `password fallback ready`:员工本人已设置并掌握密码;它是企微异常时的独立回退,不是首次 binding 的前置条件。
+2. `password fallback ready`:员工本人可通过受控自助路径设置并掌握密码;它是企微异常时的独立回退,不是首次 binding 的前置条件。本轮代表性生产验收不等于每人都已经执行该路径。
inactive/archived 账户不进入本轮登录覆盖率分母。
@@ -66,7 +66,7 @@ inactive/archived 账户不进入本轮登录覆盖率分母。
- 员工自助重设密码后旧密码和旧会话按设计失效,无需回滚到旧密码。
- 新 binding 若经本人验证仍错误,只能在精确确认原/目标映射后使用现有解绑/绑定路径恢复;不得直接删除数据库记录。
-- 任一员工阻断不影响其他员工逐人完成,但全员验收保持未完成并记录缺口计数。
+- 任一员工未来出现阻断时不影响其他员工使用;按实际覆盖记录个人缺口,不把代表性系统 PASS 改写为全员真人 PASS。
## 7. Prevention
@@ -79,3 +79,13 @@ inactive/archived 账户不进入本轮登录覆盖率分母。
- 新审计动作只记录匹配方式和布尔字段,不记录完整 userid、邮箱或企微原始 payload。
- 未绑定页面不再指示员工输入密码完成正常绑定;错误只显示稳定类别,完整身份资料不得进入 URL、日志或前端状态。
- 生产回填先 dry-run 汇总唯一匹配、无匹配和冲突计数,另行批准后才 apply。
+
+## 9. Password Input Sequential Focus
+
+认证表单采用国内连续填表优先的顺序焦点:密码输入框按 Tab 跳过框内显示/隐藏按钮并进入 DOM 中下一个顺序表单控件。登录页现有“忘记密码?”链接仍保持键盘可达;按钮继续使用原生 `button type="button"`,保留“显示密码/隐藏密码”可访问名称、按下状态和目标输入关联,鼠标与触屏行为不变;不得通过正数 `tabIndex` 人工重排表单。
+
+组件级调整同时覆盖登录、修改密码和忘记密码页,避免三个认证入口产生不同键盘习惯。测试固定顺序焦点、点击切换、可访问名称、输入值保留以及确认密码框 Enter 提交;不改变认证 API 或生产配置。
+
+## 10. Representative Password-Recovery Evidence
+
+系统能力与逐人覆盖分开记录:生产 `/ready`、认证 capabilities、SMTP 连接/认证和 1 次真人完整重置/登录共同证明共享链路可用;个人邮箱访问、投递和账号特例只有本人操作才能证明。任务收口允许系统链路 PASS,但覆盖率保持 4/6,另外 2 人标记为预计可用而非真人 PASS。
diff --git a/.trellis/tasks/07-28-all-account-login-readiness/implement.md b/.trellis/tasks/07-28-all-account-login-readiness/implement.md
index ba2d839d..008f1da4 100644
--- a/.trellis/tasks/07-28-all-account-login-readiness/implement.md
+++ b/.trellis/tasks/07-28-all-account-login-readiness/implement.md
@@ -25,7 +25,7 @@
- [x] 首批 2–3 名同事完成企微工作台真实登录试用,用户确认本轮参与者全部成功;未收集账号、userid、密码、验证码或个人明细。
- [x] 已由其余 3 名已有 binding 的 active 员工按不含账号/密码的操作说明,由本人直接测试企微工作台免登。
- [x] 每名企微成员均已从工作台真机进入;用户于 2026-08-23 确认其余 3 人全部可以进入,结合此前已通过人员,当前 6/6 唯一 binding 与真人免登验收完成。
-- [ ] 企微免登验证后,再逐人完成密码回退和首次改密状态收口;管理员不接触其密码或验证码。
+- [x] 企微免登验证后,密码回退按最终批准采用代表性生产验收;实际真人覆盖保持 4/6,另外 2 人不要求本轮逐人操作,管理员不接触其密码或验证码。
- [ ] 邮件失败者单独登记稳定失败类别;确需管理员一次性密码时先给出目标数量、审计和交付方案并另行获批。
## 4. Verification and Prevention
@@ -68,7 +68,16 @@
- [x] 初始健康等待器错误要求 Docker healthcheck,但当前 server 容器没有 healthcheck;随后以容器 running、restartCount=0、公网 `/ready=200` 以及 capabilities 通过确认服务正常,因此未回滚。
- [x] 最终 capabilities 为 `emailDeliveryReady=true`、`passwordResetEnabled=true` 且 `statusReasons=[]`;SMTP 连接与认证通过,未发送测试邮件,migration pending=none,rollback window 为 `OPEN/0`。
- [x] 临时敏感回滚副本已删除,生产 `.env` 保留已批准的启用状态;本轮未部署代码、未运行 migration、未登录员工账号、未发送邮件。
-- [ ] 由剩余 3 名首次改密人员本人逐一完成验证码重置和密码登录;管理员不接触其密码或验证码,任何额外重置、解锁或配置写入仍需另行批准。
+- [x] 剩余首次改密范围按用户最终批准采用代表性生产验收:1 人本人完成验证码重置和密码登录,另外 2 人不要求本轮逐一操作,仅记为预计可用;管理员不接触其密码或验证码。
+- [x] 用户确认其中 1 人已完整通过“忘记密码 → 设置新密码 → 使用新密码登录”;只记录通过计数,不记录邮箱、验证码或密码。当前密码历史成功覆盖按真人验收口径为 4/6,尚余 2 人。
+
+## 4D. Password Input Tab Order
+
+- [x] 调研 W3C 键盘/焦点顺序、GOV.UK 密码组件,以及 Ant Design、Arco、Element Plus、TDesign 的显示密码实现;国内组件存在“图标进入/跳过 Tab”两套默认。
+- [x] 按用户裁决调整共享 `PasswordInput`:顺序 Tab 跳过显示/隐藏按钮,保留原生按钮、动态可访问名称、按下状态和输入关联,不使用正数 `tabIndex`。
+- [x] 增加认证聚焦回归:登录页密码 → 忘记密码链接;修改密码页旧密码 → 新密码 → 确认密码 → 提交;忘记密码页新密码 → 确认密码 → 提交。
+- [x] 保留点击显示/隐藏、输入值不丢失和确认密码框 Enter 提交测试;前端 typecheck/build、3 条聚焦 E2E、Trellis/流程/敏感/差异门禁通过。
+- [x] 首次直接运行两份 E2E 文件时本地入口未启动,20/20 均以 `ERR_CONNECTION_REFUSED` 停在导航前;启动 Vite 并显式使用本地前端基址后,本次相关 3 条用例全部通过,未把环境失败归类为产品回归。
## 5. Stop Conditions
diff --git a/.trellis/tasks/07-28-all-account-login-readiness/prd.md b/.trellis/tasks/07-28-all-account-login-readiness/prd.md
index 06a33302..91a42c82 100644
--- a/.trellis/tasks/07-28-all-account-login-readiness/prd.md
+++ b/.trellis/tasks/07-28-all-account-login-readiness/prd.md
@@ -12,7 +12,7 @@
- 更新现有账号启用/入职操作门禁,防止后续 active 账户再次只有未知随机密码或未经本人验证的 binding。
- 改造企微登录缺失 binding 时的唯一企业邮箱自动匹配、事务绑定、审计和失败关闭逻辑;同步修正未绑定页面指引。
-## What I Already Know
+## Background And Confirmed Facts
- 管理员本人从企业微信工作台进入时收到“当前企业微信账号尚未绑定”,且没有已知可用密码。
- 当前登录页的未绑定流程要求先使用账号密码登录,再到个人设置完成企业微信绑定;这对不知道随机初始密码的导入账户会形成死锁。
@@ -31,11 +31,14 @@
- 本次实际失败发生在 `/reset-password` 成功之前:第二次签发使第一次验证码失效,用户收到并使用旧验证码后被后端拒绝;未发现“重置成功后被前端缓存再次送回改密页”的现场证据。
- `/email-login/code -> /email-login/verify` 是另一条邮箱验证码登录路径;现有前端在验证成功后仍遵守 `must_change_password` 跳转,而修改密码页强制要求旧密码,因此它对未知随机初始密码账户可能形成死路,但与本次失败无关。
- 2026-08-23 腾讯云生产补充只读聚合确认:active 6、binding 6,缺失、冲突、三类认证锁定和 inactive 账户活跃会话均为 0;仍有 3 个账户处于首次改密状态,历史密码成功登录覆盖为 3/6。
+- 生产邮件找回恢复后,用户确认其中 1 名首次改密人员已完整通过忘记密码、设置新密码和新密码登录;当前真人密码回退验收覆盖为 4/6,尚余 2 人。该计数待最终生产只读聚合复核,不记录个人认证材料。
+- 用户确认另外 2 人无需在本轮逐人实测:同一腾讯云生产忘记密码链路已有代表性真人全链路通过,可作为系统能力验收;另外 2 人仅记为“预计可用、未真人密码登录验收”,不得扩大表述为 6/6 真人密码回退完成。
+- 真人操作发现密码框输入完成后按 Tab 会先聚焦框内的显示/隐藏按钮,而不是下一个输入项。国内组件并无单一默认:Ant Design、Arco 当前默认让图标进入顺序焦点,Element Plus、TDesign 默认跳过;用户裁决本 OA 优先采用国内常见的连续填表习惯。
- 企业微信成功审计历史覆盖为 4/6、近 7 天为 1/6;该计数只代表实际经过 OAuth 成功回调的审计覆盖,不能替代或否定用户确认的 6/6 真人工作台可进入结果,已有会话直接进入不会必然新增 OAuth 审计。
-## Requirements (Evolving)
+## Requirements
-- 登录基线固定为双路径:每个 active 账户都必须具备可实际交付并完成首次改密的账号密码回退;属于当前企业微信通讯录的 active 员工还必须具备唯一 binding 和工作台免登。
+- 登录基线固定为双路径:每个 active 账户都必须具备可由本人通过受控自助路径完成首次改密的账号密码回退能力;属于当前企业微信通讯录的 active 员工还必须具备唯一 binding 和工作台免登。实际真人覆盖与系统能力分开记录。
- 先以只读方式核对所有 active 账户的状态、可登录标识、密码是否为强制首次改密状态、登录锁定、企业微信 binding、最近成功登录类型和企业邮箱验证码能力;只输出计数和脱敏状态。
- 把“数据库里存在密码哈希”与“员工实际掌握可用凭据”区分开,后者必须由一次真实登录或受控的一次性密码交付证明。
- 管理员本人先通过已批准的受控 break-glass 恢复一条安全登录路径,再完成企业微信绑定和工作台真机登录,不通过聊天、命令参数、环境变量、Trellis、日志或仓库传递密码。
@@ -44,13 +47,14 @@
- 企业邮箱缺失、无 active OA 匹配、目标账户已绑定另一 userid、userid 已绑定另一账户或企微资料接口失败时必须失败关闭;姓名只用于人工核对,不得作为自动绑定键。
- 自动匹配只允许绑定既有 active OA 账户,不得在普通登录中自动创建员工、修改角色、更新通讯录或覆盖冲突 binding。
- 通讯录/HR 主数据同步应在账号启用前建立 binding;登录时自动补绑定仅作为确定性兜底,不替代入职门禁。
-- 对每个 active 账户建立可验收的账号密码回退;属于当前企业微信通讯录的 active 员工还应具备唯一 binding 和工作台免登。
-- 当前 4 个 active 账户逐人验收后,把“密码回退已完成 + 企微成员免登已完成”写入新账号启用/入职门禁,避免后续导入账户再次只有未知随机密码。
+- 系统级账号密码回退以腾讯云生产代表性真人全链路验证收口;未逐人操作的 active 账户只记“预计可用”,不虚构本人密码登录证据。属于当前企业微信通讯录的 active 员工仍应具备唯一 binding 和工作台免登。
+- 把“密码回退能力已验证 + 企微成员免登已完成”写入新账号启用/入职门禁,避免后续导入账户再次只有未知随机密码;逐人真人覆盖单独记录,不以系统能力验证替代个人操作事实。
- 密码重置必须使用现有审计接口或经单独批准的受控 break-glass 路径;生成的一次性密码仅在受控现场显示一次,首次登录立即改密。
- 任何 binding 修正必须先证明目标 OA 员工和企微 userid 唯一对应;不得按姓名模糊匹配,不得修改企业微信通讯录成员。
- 生产 binding、密码、账户状态或认证配置写入前另行给出精确变更清单并取得批准。
- 最终业务职责以用户最新确认口径为准:`majianyong` 是 `ceo`,`wangshunli` 仅承担系统开发和系统管理,不是业务审批人;当前唯一 active `manager` 的 OA 直属主管应为 `majianyong`。
- `majianyong` 本人报销不配置直属主管、不允许自审,目标由 active 非本人 `finance` 审批;当前实现只支持 `finance_mgr` 且生产无持有人,本轮保持失败关闭并记录后续最小权限修正,不修改任何角色、邮箱、密码、binding、审批规则或企微通讯录。
+- 认证页密码框的顺序 Tab 行为采用“当前密码输入框 → DOM 中下一个顺序表单控件”;框内显示/隐藏按钮保留鼠标、触屏和辅助技术语义,但不占普通连续填表的 Tab 次序。登录页的“忘记密码?”链接仍保持键盘可达,不通过正数 `tabIndex` 重排其他控件。
## Decision: Credential Recovery
@@ -66,27 +70,36 @@
**Decision**:采用“双路径基线”。账号密码是每个 active 账户的独立回退;企业微信成员在此基础上必须完成工作台免登。
-**Consequences**:需要逐人安全交付一次性密码并完成首次改密,也需要逐人企微真机验收;实施工作略多,但能避免任何单一入口故障把员工锁在 OA 外。
+**Consequences**:双路径能力仍是账号启用基线;企微免登逐人验收,密码找回的本轮生产验收则由后续“代表性密码找回验收”决策细化。任何单一入口故障都不应把员工锁在 OA 外。
-## Acceptance Criteria (Evolving)
+## Decision: Representative Password-Recovery Acceptance
+
+**Context**:腾讯云生产邮件找回能力已恢复,且 1 名剩余首次改密人员已由本人完整通过忘记密码、设置新密码和新密码登录;其余 2 人走相同代码与生产配置路径。继续强制逐人测试会增加协调和验证码操作,但能覆盖个人邮箱访问、投递与账户特例。
+
+**Decision**:按用户批准,以该次生产代表性真人全链路作为系统级密码找回验收;其余 2 人不要求本轮逐人实测,状态固定为“预计可用、未真人密码登录验收”。
+
+**Consequences**:本任务可按系统能力收口,但事实记录永久保留实际覆盖 4/6,禁止写成全员真人密码回退 6/6。两人未来实际使用时若出现邮箱投递、锁定或账号特例,按新的现场事实单独诊断。
+
+## Acceptance Criteria
- [x] 管理员本人能使用一次性凭据完成首次改密,且凭据未进入聊天、任务材料、日志或仓库。
- [x] 管理员本人能从企业微信工作台直接进入 OA,不再出现未绑定提示。
- [x] 一个“无 binding、企微企业邮箱唯一匹配 active OA 账户”的合成场景能在不输入 OA 密码的情况下自动建立 binding、写审计并直接登录。
- [x] 企业邮箱缺失、无匹配、inactive、userid/binding 冲突等负向场景全部失败关闭,且不创建账户、不改角色、不按姓名绑定。
-- [ ] 所有 active 账户均有一次实际账号密码登录验收或等价的安全凭据交付与首次改密证据。
+- [x] 腾讯云生产忘记密码、设置新密码和新密码登录已由代表性真人完整通过;实际真人密码回退覆盖为 4/6,另外 2 人按用户批准记为预计可用、未逐人实测,不宣称 6/6 真人密码登录。
- [x] 所有属于当前企业微信通讯录的 active 账户均有唯一 binding,并完成本人企微真机登录验收;用户于 2026-08-23 确认其余 3 人全部可以进入,当前 6/6 真人企微主入口通过。
-- [x] 不存在 active 账户同时缺少可交付密码回退和可用企微 binding;当前 6/6 均已有经本人验证的企微主入口,但密码回退仍需单独收口。
+- [x] 不存在 active 账户同时缺少密码找回能力和可用企微 binding;当前 6/6 均已有经本人验证的企微主入口,密码找回系统能力已有代表性生产 PASS,实际真人密码回退覆盖为 4/6。
- [ ] 所有重置、绑定、解锁、会话撤销均有 OA 审计且未扩角色、未修改企业微信通讯录。
- [x] 当前唯一 active `manager` 的直属主管唯一解析为 active `majianyong`,不存在自环或主管链环路,并有脱敏审计;`wangshunli` 不进入业务审批链。
- [x] `majianyong` 保持 `ceo` 且无直属主管;其本人报销目标为 active 非本人 `finance`,当前 `finance_mgr` 配置缺口未修复前保持阻断且不发生自审。
-- [x] 自动化、Trellis、敏感扫描、生产 `/ready` 和关联认证 smoke 已通过,可以通知剩余首次改密人员逐人测试。
+- [x] 自动化、Trellis、敏感扫描、生产 `/ready` 和关联认证 smoke 已通过;未实测人员未来需要时可使用同一找回流程,但本轮不要求逐人操作。
+- [x] 登录、修改密码和忘记密码页的密码框按 Tab 跳过框内显示/隐藏按钮并进入 DOM 中下一个顺序表单控件;显示/隐藏按钮仍可点击、保留稳定可访问名称,且切换不丢失输入值。
-当前员工可测试边界:生产邮件能力、公网 `/ready`、认证 capabilities 与 SMTP 连接/认证已通过;剩余 3 名首次改密人员可由本人逐一进入忘记密码流程。不得在聊天中传递密码或验证码,不连续点击重发;新签发验证码会使旧验证码失效。
+当前员工可测试边界:生产邮件能力、公网 `/ready`、认证 capabilities 与 SMTP 连接/认证已通过;已有 1 名首次改密人员完成全链路,另外 2 人按批准不要求本轮逐人实测,仅记为预计可用。其本人未来使用找回流程时仍不得在聊天中传递密码或验证码,不连续点击重发;新签发验证码会使旧验证码失效。
## Definition of Done
-- 只读基线、写入前后差异、逐人登录验收和剩余阻断均以脱敏计数记录。
+- 只读基线、写入前后差异、代表性生产验收、实际真人覆盖和剩余阻断均以脱敏计数记录。
- 相关后端测试、前端 typecheck/build、认证 smoke、Trellis 治理和敏感扫描通过。
- 生产变更具备最小回滚:binding 修正可恢复原映射,密码重置通过 token version 和会话撤销确保旧凭据失效。
- 现有管理员手册和试运行事实源与最终登录基线一致。
@@ -94,7 +107,7 @@
## Test Plan
- 生产前后只读聚合:active 总数、必须改密数、锁定数、binding 数和各成功登录类型覆盖数。
-- 管理员本人及其余 active 员工分别完成真实密码登录与企业微信工作台免登;人工证据只记录 PASS/BLOCKED 和稳定原因。
+- 企业微信工作台免登按 6/6 真人确认记录;密码找回按代表性生产全链路与实际覆盖 4/6 分开记录,未实测 2 人只标记预计可用。人工证据只记录 PASS/BLOCKED 和稳定原因。
- 若发生管理员一次性密码兜底,核对强制改密、旧会话撤销、token version 和审计增量,不记录密码正文。
- 运行认证聚焦后端测试、前端 typecheck/build、生产 `/ready` 与认证 capabilities、公网只读 smoke。
- 运行 Trellis task/governance、敏感扫描、文档审计、流程合同和 `git diff --check`。
@@ -105,6 +118,7 @@
- 不把真实密码、完整账号、手机号、邮箱、企微 userid 或认证原始 payload 写入任务证据。
- 不启用自助 OAuth onboarding,不重构认证架构,不放宽登录、绑定或账户管理权限。
- 本轮不修改“邮箱验证码登录后首次改密仍要求旧密码”的认证流程。
+- 本轮 Tab 优化不改变密码校验、提交、自动填充、验证码、邮件投递或登录接口。
- 不启用会自动创建 OA 员工的通用企微 OAuth self-provisioning;本轮普通登录只允许匹配并绑定既有 active OA 账户。
- 不顺带导入剩余通讯录成员、修改角色或业务审批规则;主管字段仅包含用户最终批准的单条 `manager -> majianyong` OA 关系。
diff --git a/.trellis/tasks/07-28-all-account-login-readiness/task.json b/.trellis/tasks/07-28-all-account-login-readiness/task.json
index ebbde8c6..bea7e8fe 100644
--- a/.trellis/tasks/07-28-all-account-login-readiness/task.json
+++ b/.trellis/tasks/07-28-all-account-login-readiness/task.json
@@ -21,6 +21,6 @@
"children": [],
"parent": null,
"relatedFiles": [],
- "notes": "首次企微免密登录代码已进入生产,当前 6/6 真人企微主入口通过;生产只读聚合确认 active/binding 6/6,锁定、冲突和异常活跃会话均为 0。2026-08-23 现场发现邮件找回因生产开关关闭而不可用,用户批准后已在腾讯云真实生产启用;公网 /ready、认证 capabilities、SMTP 连接与认证均通过,未发送测试邮件,未部署代码或运行 migration,且保持 server=1、worker=0 的既有拓扑。当前仍有 3 人处于首次改密状态,任务继续保留,等待其本人逐一完成验证码重置和密码登录;管理员不接触密码或验证码。",
+ "notes": "首次企微免密登录代码已进入生产,当前 6/6 真人企微主入口通过;生产邮件找回经批准恢复且代表性真人全链路通过,实际真人密码回退覆盖为 4/6,另外 2 人按批准仅记为预计可用。认证密码框顺序 Tab 已按国内连续填表习惯在本地调整:跳过显示/隐藏按钮并进入下一顺序表单控件,同时保留点击、动态可访问名称、按下状态和输入关联;前端构建、3 条聚焦 E2E 与治理门禁通过。代码尚未提交、未部署生产。",
"meta": {}
}
diff --git a/client/src/components/ui/password-input.tsx b/client/src/components/ui/password-input.tsx
index 6249ad85..43977cae 100644
--- a/client/src/components/ui/password-input.tsx
+++ b/client/src/components/ui/password-input.tsx
@@ -27,9 +27,12 @@ export const PasswordInput = React.forwardRef setVisible((current) => !current)}
+ tabIndex={-1}
>
diff --git a/client/tests/e2e/login-portal-ux.spec.ts b/client/tests/e2e/login-portal-ux.spec.ts
index 76a58e18..a6431e4c 100644
--- a/client/tests/e2e/login-portal-ux.spec.ts
+++ b/client/tests/e2e/login-portal-ux.spec.ts
@@ -381,9 +381,18 @@ test("登录页提供找回密码、密码显隐,PWA 更新不会清空正在
await page.keyboard.press("Tab");
await expect(page.getByTestId("login-password")).toBeFocused();
await page.getByTestId("login-password").fill("PasswordA1234");
+ await page.getByTestId("login-password").focus();
+ await page.keyboard.press("Tab");
+ await expect(page.getByRole("link", { name: "忘记密码?" })).toBeFocused();
await expect(page.getByTestId("login-password")).toHaveAttribute("type", "password");
- await page.getByRole("button", { name: "显示密码" }).click();
+ const passwordToggle = page.locator('button[aria-controls="login-password"]');
+ await expect(passwordToggle).toHaveAttribute("aria-label", "显示密码");
+ await expect(passwordToggle).toHaveAttribute("aria-pressed", "false");
+ await expect(passwordToggle).toHaveAttribute("tabindex", "-1");
+ await passwordToggle.click();
+ await expect(passwordToggle).toHaveAttribute("aria-label", "隐藏密码");
+ await expect(passwordToggle).toHaveAttribute("aria-pressed", "true");
await expect(page.getByTestId("login-password")).toHaveAttribute("type", "text");
await expect(page.getByTestId("login-identifier")).toHaveValue("user@example.test");
await expect(page.getByTestId("login-password")).toHaveValue("PasswordA1234");
@@ -519,11 +528,18 @@ test("找回密码流程使用邮箱验证码重置新密码", async ({ page })
await page.getByTestId("forgot-password-new").fill("NewPassword1234");
await page.getByTestId("forgot-password-confirm").fill("NewPassword1234");
+ await page.getByTestId("forgot-password-new").focus();
+ await page.keyboard.press("Tab");
+ await expect(page.getByTestId("forgot-password-confirm")).toBeFocused();
+ await page.keyboard.press("Tab");
+ await expect(page.getByTestId("forgot-password-submit")).toBeFocused();
+
await page.getByRole("button", { name: "显示密码" }).first().click();
await expect(page.getByTestId("forgot-password-new")).toHaveAttribute("type", "text");
await expect(page.getByTestId("forgot-password-new")).toHaveValue("NewPassword1234");
- await page.getByTestId("forgot-password-submit").click();
+ await page.getByTestId("forgot-password-confirm").focus();
+ await page.keyboard.press("Enter");
await expect(page).toHaveURL(/\/login$/);
expect(resetPayloads).toEqual([
diff --git a/client/tests/e2e/user-onboarding.spec.ts b/client/tests/e2e/user-onboarding.spec.ts
index 1242e430..dd1d4aff 100644
--- a/client/tests/e2e/user-onboarding.spec.ts
+++ b/client/tests/e2e/user-onboarding.spec.ts
@@ -417,6 +417,15 @@ test("E2E 首次登录:一次性口令登录后强制改密", async ({ page })
await page.getByTestId("change-password-old").fill("ResetA1abc123def456");
await page.getByTestId("change-password-new").fill("NewPassword1234");
await page.getByLabel("确认新密码").fill("NewPassword1234");
+
+ await page.getByTestId("change-password-old").focus();
+ await page.keyboard.press("Tab");
+ await expect(page.getByTestId("change-password-new")).toBeFocused();
+ await page.keyboard.press("Tab");
+ await expect(page.getByLabel("确认新密码")).toBeFocused();
+ await page.keyboard.press("Tab");
+ await expect(page.getByTestId("change-password-submit")).toBeFocused();
+
await page.getByTestId("change-password-submit").click();
expect(changePasswordPayload).toEqual({