diff --git a/DEVLOG.md b/DEVLOG.md
index ba0e4ce..152ee8e 100644
--- a/DEVLOG.md
+++ b/DEVLOG.md
@@ -30,6 +30,30 @@
---
+## 2026-08-27 · PMS 加登录与两个角色(系统管理员 / 交易员),认证接第三方 bshop
+
+**背景**
+管理页面之前没有任何登录,谁开着 38100 都能看全部持仓、下命令、改参数、跑运维。这次加一层最小限度权限:先登录才能进,进来分两种人——交易员管日常交易,系统管理员在此之上再管系统维护(回放 / 对账 / 日终 / 日报 / ws 通道 / 行业导入这些运维操作)。登录不自己造账号,接第三方登录系统 bshop.bmbs.tech(手机号 + 密码,POST /authProxy/api/auth/login,multipart)。关键在于它登录成功的返回里直接带这个人的权限清单 permissions(每条 system_key:menu_key)和组织 orgs,所以角色能从登录返回里直接读出来,PMS 不必碰 stock_vix 的库,两个系统解耦。用户拍板两条:一,角色的边界按前端两块视图划分——交易员工作台上能点到的都归交易员,只有运维视图里的维护操作归系统管理员;二,一个人可以同时持有两个角色,两个权限都占就两个角色都显示、都生效,不折叠成一个。
+
+**做了什么**
+一,认证与角色。新模块 app/web/auth.py 全是纯函数(不连库,只有 call_bshop_login 一处碰网络,便于离线单测):parse_bshop_login 解析 bshop 返回(判 code==200,抽手机号 / 用户名 / 权限 / 组织);roles_from_permissions 按权限**集合**判角色——占管理员标记就有系统管理员,未配交易员准入(留空 = 人人是交易员)或占了交易员权限就有交易员,两者都不占则返回空列表 = 拒绝进入;is_admin 供各处统一判定。
+二,会话。PMS 自签会话票(base64 装 {phone, username, roles, exp} 再接 HMAC-SHA256,只用标准库不引新依赖),放 HttpOnly cookie,服务端不存会话。密钥 PMS_SESSION_SECRET 只从 .env 注入;开着登录却为空时服务一律拒绝放行(失败即关门,绝不用空密钥签票)。
+三,拦截。app/web/main.py 加一个中间件加三个接口(POST /api/auth/login、POST /api/auth/logout、GET /api/me)。判定用「管理员黑名单」:/api/ops/* 全族、/api/ws-channel/*、/api/industry/* 三个前缀,外加一条精确的 POST /api/proposals(人工补录提议的造数调试口),命中要系统管理员,其余已登录的人都放行——交易员的能力跟着他那一页走。中间件里只 return JSONResponse,不 raise(在这里抛异常会变成 500 而非预期的 401 / 403)。
+四,前端。app/web/static/index.html:未登录弹全屏登录框;顶栏显示登录人和他持有的**全部**角色,带退出按钮;非管理员锁在交易员视图、顶栏「运维」切换按钮对他不显示,运维那一整块因此够不到;页面所有请求的统一出口函数里分开处理 401(弹登录框)与 403(提示无权限),业务用的 ok() 失败回 200 与这套真状态码互不影响。
+五,顺带翻出并修掉一个隐藏 bug:早前几处「系统管理员」被多写了两字前缀写成了别的词,其中前端判管理员那行拿这个写错的角色名去比后端发的「系统管理员」永远比不中——即按旧码任何人登录都不会被认成管理员、运维入口谁都看不到。这类只错在字符串值上、语法完全正常,靠语法检查发现不了,这次全仓库统一修正并加脚本复查。
+
+**动了哪些文件**
+app/web/auth.py(新增:认证 / 角色集合 / 会话签验 / 接口鉴权,纯函数);app/web/main.py(登录中间件加三个鉴权接口,登录与 /api/me 改读写 roles 列表,中间件按 is_admin 判定);config/settings.py(新增 PMS_AUTH_ENABLED / SESSION_SECRET / SESSION_TTL_HOURS / SESSION_COOKIE / BSHOP_LOGIN_URL / BSHOP_TIMEOUT / ADMIN_PERMS / TRADER_PERMS 八个显式字段);app/web/static/index.html(登录框 + 顶栏多角色显示与退出 + 锁运维视图 + 401/403 处理);.env.example(加 PMS_SESSION_SECRET 密钥段与两个角色标记 PMS_ADMIN_PERMS / PMS_TRADER_PERMS);scripts/test_batch18_units.py(新增 16 例:多角色判定含同占两角色、会话签验、bshop 解析、接口鉴权维护类归管理员、公开白名单);scripts/run_tests.py(注册 batch18)。
+
+**部署方式**
+桥机 factorevaluation 上 make deploy(源码打进镜像),make test 见 ALL SUITES PASS。上线前 .env 必须加一行 PMS_SESSION_SECRET(用 python -c "import secrets; print(secrets.token_urlsafe(48))" 生成);角色标记 PMS_ADMIN_PERMS(默认 pms:admin)与 PMS_TRADER_PERMS(留空 = 任何登录人都是交易员)可按需在 .env 指到测试账号真有的权限,方便联测。新增 env 记住要 force-recreate 才重读。回滚一行:.env 设 PMS_AUTH_ENABLED=false 重启,页面即回到无登录旧行为。另需在 stock_vix 的「用户与权限」页加一条管理员标记权限(建议 system_key=pms、menu_key=admin)并绑给该当管理员的账号。
+
+**真机判收**
+未判收。本地纯逻辑单测 16 例 ALL PASS(角色集合、会话签验、bshop 解析、接口鉴权)。真正连 bshop 的那一跳,桥机与开发机都被各自出网白名单拦掉,只能在能连通 bshop 的部署机联调:先 curl bshop 登录看回 code==200 加 access_token;再走 PMS 自己的 /api/auth/login 拿 cookie——未登录访问受保护接口应 401、带 cookie 应 200、交易员点 /api/ops/* 应 403、同时持两权限的账号 /api/me 回 roles 两个。
+
+**还欠着什么**
+桥机联调真连 bshop 的登录闭环(上面几条 curl)。另:交易员操作日志 op_log 的 by 目前仍写死「user」,没接登录人;要审计到人的话,后续把中间件里的 request.state.user 接进 _oplog。
+
## 2026-08-25(续)· 策略自动挂载:个股打法状态机(吸筹挂网格 / 高热挂止盈 / 转派发暂停买入 / 接力切换)
**背景**
diff --git a/README.md b/README.md
index c1ca30c..f432bd8 100644
--- a/README.md
+++ b/README.md
@@ -138,6 +138,14 @@ docker compose logs -f pms-ws
1. **前端资源走 unpkg CDN**,浏览器需能访问外网。上不了外网的表现不是报错,而是**只剩一个头部、下面全空**——`el-*` 组件全部渲染不出来。离线办法:把 Vue3 / ElementPlus / axios 三个库放进 `app/web/static/vendor/`,再把 `index.html` 头部那 5 个 URL 换成 `/static/vendor/xxx`(后端已把 `static` 挂在 `/static`,页面本身无构建步骤)。
2. **模板里不能用自闭合的自定义标签**。`` 这种写法浏览器 HTML 解析器**不认**——只有 `br`/`img`/`input` 这类 void 元素和 SVG 才认自闭合。写成 `` 会被当成开标签,**后面所有内容都变成它的子节点**;若它带 `v-if` 且条件为假,整页就跟着消失。2026-07-29 页面白屏即此故(75 处自闭合,第一处 `` 把整页吞掉了)。一律写成 ``。
+**管理页面的登录与权限(2026-08 加)**:页面现在必须先登录才能进。登录接第三方系统 bshop(手机号加密码),校验通过后 PMS 自签一张会话票放进 cookie,之后每个请求验票。角色分两种——交易员管日常交易,系统管理员在此之上再管系统维护(运维视图里的回放 / 对账 / 日终 / 日报 / ws 通道 / 行业导入);一个人可以同时持有两个角色,都占就都生效、都显示。
+
+1. 上线前 `.env` 必须加一行会话密钥 `PMS_SESSION_SECRET`(`python -c "import secrets; print(secrets.token_urlsafe(48))"` 生成)。开着登录(`PMS_AUTH_ENABLED` 默认 true)却留空时,服务一律拒绝放行。
+2. 角色标记两项也在 `.env`:`PMS_ADMIN_PERMS`(默认 `pms:admin`,登录返回的权限里含它就带上系统管理员这个角色)、`PMS_TRADER_PERMS`(留空 = 任何能登录的人都是交易员)。联测时可临时指到测试账号真有的权限。
+3. 谁是管理员由权限决定:在 stock_vix 的「用户与权限」页加一条管理员标记权限(建议 system_key=`pms`、menu_key=`admin`)并绑给该当管理员的账号即可。
+4. 回滚一行:`.env` 设 `PMS_AUTH_ENABLED=false` 重启,页面即回到无登录旧行为。
+5. 细节见 DEVLOG.md 的 2026-08-27 节点与 `.env.example` 里的两段说明。
+
**依赖版本坑**:`redis` 必须锁在 5.x。redis-py 6.x 默认按 RESP3 握手(先发 `HELLO 3`),而 208/150 两台 Redis 是 6.0 以前的版本,会回 `unknown command HELLO`,行情与 Celery 总线一起断。`requirements.txt` 已锁版本,代码侧另有 `protocol=2` 双保险;升级依赖时别把这条放开。
## 调度总表(`--profile sched` 生效,设计 §10)
@@ -310,7 +318,7 @@ make t-gate # 随时: 规则闸/研判闸拒了什么、为什么
## 已实现 / 待开发
-**已实现**:建表 DDL 与建表脚本;配置与运行参数中心;仓位规划器与安全垫账;命令系统(27 类命令全目录 + 双状态机 + 冲突识别);方案生成器(降仓凑额四档、升仓、建仓分批、清仓/减至、行业清仓与限额、暂停买入撤单);账本回放与对账引擎(成交认领、外部成交并入 BASE 告警、以下游为准修正、除权检测、T+1 可用量、连续不一致升级);规则闸终检;择时执行器实现 B(分日配额、分笔、VWAP/回踩/不追高、14:45 兜底、停牌一字板顺延、窗口耗尽收口、挂单有效期);动作引擎四类自主动作 + 研判闸客户端 + 提议分流;决策系统信号消化(两条流独立消费组订阅、置信度分档转清仓指令或提议);管理页面四块 + 运维/日报抽屉;调度器九个调度位;**上游选股计划接口接入**(`/plan` 取候选池、交易日龄硬校验、`theme` 灌行业映射表、页面预览抽屉与不可用横幅);**榜单变化提示**(名册快照 + 新进/掉榜/档位升降/覆盖翻转/名次跳变,持仓票单列,榜尾截断噪音闸);**ws 直连通道的连接层**(常驻进程 + 出口队列 + 签名 + seq 水位与累积确认,见下);**择时实现 A 客户端**(`exec_advisor.py`:本地检查先行 → 问决策系统「现价在不在执行区间」→ 应答缓存与失败冷却 → 不可用退实现 B,默认 `PMS_EXEC_IMPL=B` 整个短路);**单测 425 例**。
+**已实现**:建表 DDL 与建表脚本;配置与运行参数中心;仓位规划器与安全垫账;命令系统(27 类命令全目录 + 双状态机 + 冲突识别);方案生成器(降仓凑额四档、升仓、建仓分批、清仓/减至、行业清仓与限额、暂停买入撤单);账本回放与对账引擎(成交认领、外部成交并入 BASE 告警、以下游为准修正、除权检测、T+1 可用量、连续不一致升级);规则闸终检;择时执行器实现 B(分日配额、分笔、VWAP/回踩/不追高、14:45 兜底、停牌一字板顺延、窗口耗尽收口、挂单有效期);动作引擎四类自主动作 + 研判闸客户端 + 提议分流;决策系统信号消化(两条流独立消费组订阅、置信度分档转清仓指令或提议);管理页面四块 + 运维/日报抽屉;**登录与两角色**(系统管理员 / 交易员,认证接 bshop,可同时持有);调度器九个调度位;**上游选股计划接口接入**(`/plan` 取候选池、交易日龄硬校验、`theme` 灌行业映射表、页面预览抽屉与不可用横幅);**榜单变化提示**(名册快照 + 新进/掉榜/档位升降/覆盖翻转/名次跳变,持仓票单列,榜尾截断噪音闸);**ws 直连通道的连接层**(常驻进程 + 出口队列 + 签名 + seq 水位与累积确认,见下);**择时实现 A 客户端**(`exec_advisor.py`:本地检查先行 → 问决策系统「现价在不在执行区间」→ 应答缓存与失败冷却 → 不可用退实现 B,默认 `PMS_EXEC_IMPL=B` 整个短路);**单测 425 例**。
### 静默失败专项(2026-07-31,八条已修)