ek-ui 0.2.27

ek 生态常驻主界面(egui/eframe):插件即界面——观测页 + 日志页,车头与 UI 同插件
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
# DESIGN —— ek-ui(S1b 常驻观测界面)

Frozen ref: docs/ek-ui-station-plan.md(DV 表 + S1b 段验收)。本档只记**结构决定**与
执行期钉。形式沿用 ek-dm/ek-station 的 BD-U1.. 编号加序。

## 结构

- `state.rs`:`UiShared`(总线面写 / 窗口面读)+ `LogRing`(cap 512,挤旧 +
  spilled 单调计数)+ `BusSnap`(svctl.list 行 + 逐插件服务 id 目录)。
  锁粒度:每次操作 lock/clone,不跨帧持锁;`snapshot()` 返回 `Arc<BusSnap>` 零拷贝。
- `plugin.rs`:`UiPlugin`("ui"):routes = `eLog`/`ePlugUp`/`ePlugDown`/`eTimer` +
  `eTick.`(行情带唯一源)+ `eContract.`/`eContractFinish`(查询合约窗数据流,
  ek-ctp 登录后自动查询,UI 只收不催)。
  - eLog:payload `LogData`,`is_warning_or_above` 打 WARN 标签(+UCH 转储环形);
  - ePlugUp/Down:payload `PlugNotice` → 单行 host 日志 + rev;
  - eTick./eContract.:payload 直落 `tick_store`/`contract_store`(键 EX.symbol);
  - eTimer:`ask(svctl.list, 2s)` + 逐插件 `ctx.services_of` → `set_snapshot`;
    ask 失败进日志面,不堆 panic。
- `render.rs`:`render_body(ctx, &UiShared)` 唯一渲染入口(L2;v5 面板化,
  BD-UI-27/29):TopBottomPanel(菜单)→ TopBottomPanel(行情带,resizable)→
  SidePanel(下单面板 + 盘口,resizable)→ CentralPanel(2×2 监控格,响应式)→
  弹窗面(K线/运维/CTP/查询合约,egui::Window,面板之后)。
- `runtime.rs`:
  - `ui_kinds(&Arc<UiShared>)` = station 缺省表 + ui 工厂;工厂闭包绑定**同一个**
    shared → ui 插件被 svctl 摘除/再注,窗口观测态不丢(状态在 shared 不在插件体);
  - `spawn_host_watcher`:`sleep 120ms` 轮询 stop 位 → `engine.close_all(3s)`;
    close 顺序(逆 start 序)仍由宿主引擎决定,观察线程只送信号;
  - `run_station_ui`:boot(不 start 由 boot 内部? 否—— boot 只装配)→ `engine.start()`
    → 观察线程 → eframe `run_native`;关窗 `on_close_event → request_stop` →
    join 观察线程 → 返回。启动失败(start Err)以 `Err(String)` 上抛(供 S1c
    launcher 的失败信号,不在窗口内吞错)。

## 执行期钉

- **BD-UI-9(UI 即插件 / 双线程形貌)**:总线 worker 写、eframe 主线程读,以
  `Arc<UiShared>` 相连;不加自制通道、不退回向量共享消息队列。
- **BD-UI-10(L2 单渲染入口)**:`render_body(ctx, &UiShared)` 被真实 App 与
  kittest 同调;面板组合在 render_body 内(BD-UI-29),测试走
  `Harness::builder().build(|ctx| …)`(ctx 级,不再包 CentralPanel)。
- **BD-UI-7(四件套版本锁)**:eframe = egui = egui_kittest = `0.33.3`;
  egui-charts 0.2.0 进入点定为 S4(K 线页),进入时与四件套重验一版。
- **BD-UI-11(关窗协议)**:`App::on_close_event` 只置 stop 位;close_all 由
  观察线程完成 —— 窗口永不直接持有 `MainEngine::close_all` 调用链。
- **BD-UI-12(kittest 用法纪律,源自官方 README)**:优先节点/文本断言,少用
  图像快照;快照只对静态首帧;更新基线仅经 `UPDATE_SNAPSHOTS='true'`;冲突
  先 diff 图后压缩容差。
- **BD-UI-13(纯观测零服务)**:ui 插件 `services()` 返回空;管制面唯一在 svctl
  —— 界面无特权路径。动态 add 的 ask 窗口期(`on_init` 后)与 headless 同一契约。
- **BD-UI-14(启动器协议,与 ek-station bin `--from/--ready-file` 同口径)**:
  清单 = 行式文本(`#` 注释;空行跳过;行内空白拒);就绪标记 = `ready\n`;
  spawn 分离式(stdin/stdout 静默 + Windows `CREATE_NO_WINDOW` + stderr 管道,
  **stderr 生存期 ≤ 64KB** —— 子进程不朝 stderr 泼流,违约即协议故障);
  等待 = 50ms 轮询标记 + 存活检查,30s 上限(超时 kill);失败分类 `Exited{code,
  stderr_tail}` / `NoReadySignal{timeout}`;成功路径 **drop Handle = 分离脱管**。
- **BD-UI-15(清单解析地面)**:pick.rs 与 ek-station main.rs 的 `--from` 解析
  S1c 各自落地(等语义);S3 toml-lite 重构时统一抽到共享模块(单点 PR),避免
  现在跨 crate 发版拖 III 制订的节奏。
- **BD-UI-16(pick 窗两线程形貌)**:UI 线程只画与翻位;spawn+wait 在工作线程
  (30s 不冻 UI);结果回槽 `RunShared`;App 见果即关窗,
  收场归 `run_pick_launcher`(join + 读槽)。ui 插件自动补入 pick 后配置,
  其余勾选即装配面(与 boot 同表)。
- **BD-UI-17(隔离红标的闭环面)**:`ePlugQDown.<名>` 是定向广播(未知名无法
  预订阅 —— 严格 exact-type 路由),UI 侧红标走快照行状态扫描
  (`is_quarantined`,1s 节拍内落实);隔离行仍可重注入(svctl.start 对隔离行
  被 Rejected(重名)拒;既定语义链 = 先 remove 再 start)。红标卡件 trace:
  `ek-core::lifecycle::EVENT_PLUG_DOWN_PREFIX("ePlugQDown.")` 常量名与后缀型
  一致,但仅作 reference,不预订阅。
- **BD-UI-18(操作指令的唯一通道)**:窗口面动作只 `push_cmd(UiCmd)`,
  NOT 直 ask —— `PluginContext` 只在 ui 插件 worker 可用;eTimer 拍先辞行
  指令再拉快照;移除走 `pending_confirm` 单槽二步(Armed→Fired;Switched =
  目标迁移),取消钮可擦;重注入为单步(加性动作);拒绝类回执
  (`Rejected/NoSuchName`)进日志面板,不在 UI 抛错。
- **BD-UI-19(配置面 → 2026-10-17 用户定盘撤销,零配置口径)**:原 S3 落地过
  toml-lite + `setting/global.toml` 融入读法 + 状态条 + 脱敏(cfg.rs/cfg 测试
  9 例/s7 kittest 1 例)。**现行口径:ek-ui 零配置、宿主零配置** —— 不读任何
  配置文件,界面本身无设置;插件结构只由 `--from` 清单或 demo 缺省决定。
  需要配置的插件(唯一 = ek-ctp 虚拟账号),配置面长在插件自身,激活时在插件
  内完成(见「配置面撤销记录」);宿主/界面不代读不转发。cfg.rs 整块删除;
  脱敏纪律(redact)随配置面一同撤销(无配置输入即无秘密暴露面;插件自带
  配置面若需展示账号,脱敏规则届时由该插件自持)。
- **BD-UI-20(S4 行情面口径)**:主题面(数据)与身体面(交互)分离——
  `MarketStore`持直播线上屏(eBar./eBarUpdate.),(symbol,interval) 格缓存
  4096 丢头保尾,帧/更新/回灌三计数 = 「刷新不丢帧统计可见」的三处钉面;
  K线 chart 实例**每帧重建**(v1 不保 pan/缩放;首帧快照入库即 review 约定
  —— BD-UI-8);图表层引用在本 crate 集中(`render::render_kline_cell`
  一点换器,egui_charts→egui_plot 退守路线只动此处,BD-UI-4)。
- **BD-UI-21(S4 直播面工厂纪律)**:UI 宿主的 dm 工厂在 `ui_kinds` 内换建
  (real_time=true),ek-station headless 的 rt-off 工厂**不动** ——
  宿主差异收在各自工厂表,不回写 ek-station、不新增 svctl 控制面;
  dm 工厂 real_time 门为装配参数(缺省 true;UI 观测面要实时门 —— 非配置文件
  输入,零配置口径,BD-UI-19 现行版)。
- **BD-UI-15 兑现记录(S3)**:清单地面统一抽世到 `ek-station 0.1.1::pickfile`
  (已发布;`ek-ui::pick` 变薄转发,launcher/bin/测试共一处,双实现消灭)。
- **BD-UI-22(S5 纸面网关契约纪律)**:sim 前三口直接复用 ek-ctp 的
  Req/Reply 类型(SubscribeReq/SendOrderReq/CancelOrderReq 及回包)——
  同构 = 同一类型面,接线换 `sim.` 即插真网关;第四口 `query_rate` 为 sim
  自定(ek-ctp 0.2.0 无此口,CTP 流控语义镜像)。撮合规则钉:限价成交于
  委托价、市价成交于 close/last、bar.volume = 本拍成交预算、挂单 FIFO ——
  手数排队由此可断言。撮合器 `SimMatcher` 纯逻辑零总线(金样本直断),
  插件壳只做路由/锁/上总线(锁内撮合锁外 put,与 DmPlugin 同形)。
  sim 不引 ek-oms:成交经 eOrder./eTrade./ePosition. 总线入账由 OmsPlugin
  被动完成(插件不直调铁律不破)。
- **BD-UI-23(S5 监控页读数纪律)**:监控页读数 = ui 插件 eTimer 拍 ask
  `oms.get_all_*` 全量查询帧(与 headless 同源契约;oms 不在 → 空帧不抛错);
  下单/撤单/流控三口走 UiCmd → ask `sim.*`(窗口面零直调)。撤单钮只挂
  ACTIVE_STATUSES 行(与 ek-data 同口径)。kittest 点击纪律:**`click_accesskit`
  取代坐标 `click`** —— 坐标点击对折叠线下(clip 外)节点无效,accesskit
  动作可达不可见控件;`get_all_by_label` 精确匹配、`query_by_label_contains`
  子串匹配且多命中即炸,撞名列按语义选面。
- **BD-UI-24(S6 CTP 手工配置面,2026-10-17 集成测试战役)**:「插件需要配置 →
  配置面在插件自身」的正向界面落地 —— CTP 段 9 输入位全部**人敲**(界面与宿主
  不读文件;密码/AuthCode `password(true)` 掩码不回显),`连接 CTP` →
  `UiCmd::CtpConfigure(CtpForm)` → ask `ctp.configure`(ek-ctp 0.3.0
  D-CONFIGURE:校验/守卫复位/发起归插件,md 账号回落也在插件侧裁决 —— 表单
  `to_configure_req` 不预处理);`订阅行情` → ask `ctp.subscribe`(订阅位
  逗号/空白解析,空位红字不入列)。`UiCmd` 携 `CtpForm`(全 String 可 PartialEq;
  ek-ctp `ConfigureReq` 无 PartialEq,抬线在 ui 插件出队点一次转换,装版不动)。
  资金小节 = `oms.get_all_accounts` 帧(可用 = balance−frozen 由 AccountData
  自算)。native 经 feature `ctp-native` 透传(默认关;实连战役开,工厂
  `with_native` 在 ek-station 侧)。装配序纪律:要观察插件 on_start 的日志,
  ui 须在清单里先于该插件(观察序 = 装配序,c0 钉)。
- **BD-UI-25(票据网关选择子,2026-10-17 战役)**:票据新增「网关」位(空 =
  `sim` 纸面;填 `ctp` = ask `ctp.send_order`/`ctp.cancel_order`);`UiCmd`
  随单携 `gateway`;行级撤单钮跟随委托来路 `gateway_name`(空兜底 sim)。
- **BD-UI-26(CJK 字体回退,2026-10-18 乱码根因)**:epaint 0.33 缺省字体组
  (Hack/Ubuntu-Light/NotoEmoji/emoji-icon)**无 CJK 字形** → 中文豆腐块。
  `fonts::apply_cjk_fonts` 启动时从系统字体候选表装载一枚 CJK 字体追加
  Proportional/Monospace 两族末位(**只收纯 .ttf/.otf** —— epaint 走 ab_glyph,
  不吃 .ttc 集合且解析失败即 panic;VF 按缺省实例解析,排位靠后);两处
  run_native 取帧闭包接线(runtime + pick 窗)。验收三钉:
  f0 字形 API(22 常用汉字 has_glyph)、s16 汉字密集页快照 baseline、实窗
  stderr 证据行 `[ek-ui] CJK 回退字体已装载: <path>`。界面文案保持中文单语
  (用户定盘:要的是不乱码,不是双语切换)。
- **BD-UI-27(v4 带框架 + 订阅两入口,2026-10-18 布局重做定稿)**:
  ① **带框架(Python 主界面等价)**:菜单栏(运维|网关|查询合约|帮助)→ 行情带
  (全宽,tick 唯一源,交易所二级 tab,双击行弹 K线窗)→ 交易带(左下单面板
  9 字段 + 委托/全撤 + 右 2×2 监控格「委托|未成交」「成交」「持仓」「资金|日志」);
  回灌/票据界面面退役(总线面 UiCmd::Reload 保留);默认窗 1280×860。
  ② **订阅两入口(用户裁定「订阅不在网关窗定死」)**:下单面板代码框回车 =
  CtpSubscribe(bare symbol,Python returnPressed 等价);菜单「查询合约」弹窗
  (Python ContractManager 等价:查询框留空 = 全部 + 勾选 + 订阅选中)。CTP 网关窗
  无订阅字段。UI 对合约流只收不催(无催查询总线口)。
  ③ **egui 0.33 帧内 sizing 退让钉(实窗截图审出)**:帧内 Grid 的 sizing 预估遇
  ComboBox 高行会退让,网格后挂的面板内容会叠回网格 → 下单表单 = 网格(按钮/
  提示语都是网格行,网格后不挂内容),盘口独立帧,交易带整体包 ScrollArea 保小窗
  可达(D7 口径)。④ **kittest 钉**:建构面统一 `harness_for()` 1280×860(实窗
  同源);点击帧断言用 `harness.step()` 单帧(`run()` 空闲帧会撤走单帧红字);
  撞名 label(委托钮/tab/表题)用 `get_all_by_label` 按渲染序取。
- **BD-UI-28(第五轮审计,2026-10-18 复审修正)**:
  ① **回车订阅真缺陷(实码修复)**:egui 单行 TextEdit 遇 Enter 即
  `surrender_focus`,`has_focus()` 在检查时已 false —— 原 `has_focus +
  key_pressed` 口径下回车订阅**实窗从不触发**;改规范口径 `lost_focus() +
  key_pressed(Enter)`(s20 钉实测)。回车输入带 `EX.` 前缀则剥成裸码并回写
  表单(与查询合约窗同口径,ctp.subscribe 收裸码)。
  ② **悬空面清除**:`selected_symbol` 四处写零读(注释声称「联动下单面板」
  未兑现)→ 字段/访问器删除;行情带行**单击 = 带入下单面板**(方案「行单击
  = 选中合约」落地,s8 钉);委托行/持仓行单击无动作(Python 同口径)。
  ③ **委托行双击 = 撤单**(Python OrderMonitor `itemDoubleClicked→
  cancel_order` 等价,活动行限定,s11 钉;原实现双击弹 K线为偏离,已撤 ——
  K线入口唯一在行情带双击)。④ **表列补全(Python 蓝图逐值抄录)**:委托表
  13→17 列(+名称/交易所/类型/来源=reference)、成交表 10→12(+名称/交易所)、
  持仓表 11→16(+名称/交易所/在途/市值=market_value/成本=price);数据面
  OrderData/TradeData/PositionData 字段全在场,零新增依赖。⑤ **tick_of
  交易所感知**:交易所非空 = EX.symbol 精确匹配(跨所同码不串),空 = 仅按
  代码。⑥ **kittest 双击口径**:`step()` 每事件一帧且缺省 step_dt=0.25s →
  两击释放间隔 0.5s > egui `max_double_click_delay` 0.3s → 双击钉用
  `with_step_dt(0.1)` 建构(s11);同帧四连击不可用(每事件一帧)。Node 借用
  harness 树,取 rect/id 后再 step(借用检查);`Role::TextInput` 非 TextBox。
  ⑦ **Frame 布局泄漏钉(s21 满表快照显形)**:`Frame::show` 继承父级
  `ui.horizontal` 的横向布局 → 2×2 格内 tab 行/表题/宽网格横流挤同一行
  (空表期隐形,满表现形)→ `group_frame` 内容强制 `ui.vertical`(行情带同款
  口径:短行成行,宽网格末位溢出走横向滚动);下单面板帧不动(其内容 = 网格 +
  罕见 err label,横流反而护住 BD-UI-27 叠行钉)。委托表表题撤除(tab
  「委托|未成交」即 Python 表身份,三重撞名降为两面,s12 口径同步)。
  满表视觉钉 s21(委托 17/成交 12/持仓 16 列入 baseline)。
- **BD-UI-29(面板化重构,布局 v5;实窗演示暴露 + 官方文档核对)**:实窗
  1280 演示暴露三症:① 交易带定宽 250+140+480+480+间距 ≈ 1438 > 1280 →
  右列裁切且带级 ScrollArea 只开纵向 → 裁掉内容不可达(违 BD-UI-27 可达
  口径);② 2×2 格定高 250 实际只渲染 ~85px → 底部死区 ~300px;③ 菜单/两带
  全挤单 CentralPanel 手工嵌套,无 Python splitter 语义。
  - **机制实锤(dbg 探针)**:`[row] avail=[9984, 18]` —— egui horizontal 行
    成员的可用高度初始只有一行高(interact_size),**高度分配不沿横向向下
    传播**,行按内容收缩 → 格内 `ScrollArea.max_height` 被钳到最小滚动尺寸,
    定高失效(②的根因;①为定宽算术,1438>1280 可复现)。
  - **官方口径核对**(egui 0.33.3 源码模块文档 = docs.rs 同源;crates.io 包
    不含 examples/,egui.rs 书为 JS 渲染页):panel.rs「面板是放控件的正规
    区域;顶层面板不得互相嵌套;CentralPanel 必须最后加;Window 挂面板之后」;
    `TopBottomPanel::resizable/default_height`、`SidePanel::resizable/
    default_width` = QSplitter 官方等价;resizable 前提 = 内容吃满可用空间
    (ScrollArea auto_shrink(false) 满足)。
  - **落地**:render_body 改收 `&egui::Context` 自开四面板(菜单 → 行情带
    240 可拖 → 下单侧栏 250 可拖[下单面板 + 盘口纵排,Python TradingWidget
    内序] → CentralPanel 最后 → 弹窗);2×2 格响应式 `allocate_exact_size`
    预留 (可用宽-间距)/2 × (可用高-间距)/2 矩形 + `new_child` 在预留矩形内
    开 Frame+ScrollArea(高度才吃满;`add_sized` 收 impl Widget 不适用闭包
    内容,已试撤);`group_frame` 保留 `ui.vertical` 泄漏钉(BD-UI-28⑦ 延续)。
  - **BD-UI-28⑦「下单面板帧不动」被本钉取代**:面板化后下单侧栏内容天然
    纵向,err label 后置改由 ScrollArea 吸收 sizing 退让(BD-UI-27 口径不变,
    护垫从「横流侥幸」换成「官方吸收面」)。
  - **钉**:s3 加结构钉(右列存在贴边不裁切/底行沉底无死区/侧栏在左/两列
    等宽随窗分配,先于字体让位执行);快照 s3/s10/s16/s21 重基线;行为钉
    s0–s20 全数不动全绿;kittest 建构面 `build_ui` → `build`(ctx 级)。

## 与 ek-station 的边界

ek-ui 依赖 `ek-station = "0.1.7"` / `ek-ctp = "0.5.0"`(装版;禁 path —— 同纪律)。ek-station
本 bin 保持 headless 不引 ek-ui;UI 总装配在 ek-ui(`ui_kinds` 复用 `default_kinds`
+ ui 工厂),launcher(S1c)也在 ek-ui 侧。

## 审计修正记录(2026-10-17 全面复审)

- **BD-UI-22 补钉(撮合两守卫)**:① **tick/bar 双重计价防护** —— tick 成交
  挂账 `tick_filled`,bar 收口预算 = bar.volume − 本段 tick 已成(同一笔市场
  量只吃一次;u5 钉);② **bar 单调守卫** —— 同 symbol `bar.datetime` 非严格
  递增的帧(重复/重放)直接忽略,不重复撮合(u6 钉);③ ask 臂锁面开块
  (match 暂存守卫会跨臂存活 —— 锁内造单锁外上总线,与 on_mail 同形)。
- **ek-station 0.1.2(pickfile BOM 剥离)**:Notepad 系 UTF-8 BOM 使首行注释
  识别失效 → 整表 BadLine 拒启(活跑实测撞到);read 剥 BOM,p3 钉。
- 复审确认未动面:ek-station headless 工厂表(rt-off)未波及;已发布六仓仅
  rustfmt 1.8.0 纯格式对齐(无语义变);setting/global.toml 只读;
  ek-ctp/ek-oms 契约类型只引用未改。

## 配置面撤销记录(2026-10-17 用户定盘)

- 用户口径:**界面零配置 + 宿主零配置** —— ek-ui 不读任何配置文件;station
  侧也不预读配置转发。插件结构只由 `--from` 清单或 demo 缺省决定。
- **配置归插件自身(定盘)**:激活列表(pick 窗勾选 / svctl start 重注入)时,
  哪个插件需要配置,配置面就在该插件内部完成(插件自带配置面/配置服务);
  宿主与界面都不代读、不转发、不集中。暂时不需要任何多配置机制。
- 全生态唯一需要配置的插件 = ek-ctp(虚拟账号)。其配置面归 ek-ctp 插件自身
  (S6 接线时按此定盘落地:激活 ctp 时配置在插件内完成,不落共享配置树)。
- 落地:cfg.rs(toml-lite/UiConfig/locate_setting/redact)+ CfgStatus 状态条 +
  `--config` 形态 + 发现链整块删除;cfg 测试 9 例 + s7 kittest 1 例随撤;
  测试 51 → 41。**setting/ 目录已整树清理(2026-10-17 定盘)**:global.toml +
  backtest/common/live/test 分层样例(含凭据档)全部删除——插件配置自持,
  工作区不留共享配置文件;Python 侧 c:\dev\enkeep_trade_setting 原档不在
  清理范围(只读纪律,原样未动)。

## BD-UI-30(0.1.1 薄下单者,OMS 中枢链,2026-10-19)

- **裁定**:下单/撤单直发 `{gw}.send_order`/`{gw}.cancel_order` 的旁路口
  **删除**——UI 是薄下单者,管制面收敛到 oms 两服务:
  `UiCmd::SendOrder{req, gateway, mode}` → `oms.submit_order`(五段管线:
  入口校验/拆单槽位/风控槽位/发送段/记账段;parent 聚合 SENT|PARTIAL|REJECTED
  逐腿显形进日志);`UiCmd::CancelOrder{orderid, gateway}` → `oms.cancel_order`
  (守卫 4000 不可绕过;symbol/exchange 不再随令携带,oms 订单缓存解;
  全撤 = 界面侧逐个扇出到同一守卫口)。BD-UI-23/25 的「ask sim.*/ctp.*」
  口径自此被取代(历史记录保留)。
- **「自动开平」勾选(OrderForm.auto_split,默认开)**:拆单四态的 UI 映射 =
  Auto(勾选)/None(取消,原样旁路);风控与守卫不可旁,拆单可逐请求旁
  (DESIGN ek-oms §6.2 口径的 UI 面)。s22 钉勾选双向 → mode 逐位。
- **装配口径统一**:ui_kinds 复用 station 0.1.7 `default_kinds`——oms 已装配
  ek-risk 三闸(standard_gates 缺省限额),面板与 headless 同一台 oms。
- **fixture 退役**:ui_converter_chain 从「offset 夹具进链」升格真管线链
  (拆单器入驻 oms 管线,夹具失去存在意义;dev-dep ek-converter 删除)。
  顺带实锤忠实初始化口径:无成交史的首轮 `ePositionDone.` 只记时刻等下一轮
  (原稿 1268-1288/1282-1285),第二轮距首轮 ≥3s 才置位——链测补第二轮。
- **实连钉**:ui_ctp_live 第 13 步(有仓则 CLOSE 经管线 = CLOSETODAY 实连
  证据;7×24 快照市无成交 = OBSERVE 如实记档)。13 步 110s 全收场。
- **依赖**:ek-oms 0.2.1→0.3.1、ek-station 0.1.5→0.1.7(强制 bump,防同构建
  两份 oms)。

## BD-UI-31(0.2.0 界面体验轮,2026-10-19;用户四点裁定)

- **R1 菜单一键直达**:「运维/网关/查询合约」menu_button→下拉→入口钮的
  两层结构废除,菜单钮即入口,单击直接弹窗(「帮助」保留菜单——多行内容)。
  破坏性:三个入口钮 pub 常量删除(标签 = 菜单名)。
- **R2 成交表方向上色**:方向列空绿/多红,补齐与委托/持仓表的一致口径
  (此前成交表方向为裸文本 = 遗漏,用户实看指出)。
- **R3 表格行选中 + 悬停跳色**(`UiShared::row_selected/select_row` 新口 +
  `paint_row_bg` 渲染工具):委托/未成交/成交/持仓/查询合约窗行首格单击 =
  选中行(蓝调半透明跳底,每表一行,换行即换选);行情带选中行 = 下单面板
  当前合约(带入即跳色,不另存态);全表悬停 = 淡灰底。行首格既有双击动作
  (撤单/带入/K线)全保留——egui 双击先经 clicked,选中顺带发生,无冲突。
  叠底为半透明(行文字先画,叠后仍可读)。
- **R4 标题文字撤除**:「行情带(唯一源…)」「下单面板」「盘口(合约)」三处
  题字整行删(带位/侧栏即身份;Python TickMonitor/TradingWidget 无题同口径;
  盘口括号合约代码一并删——选中合约由下单面板代码格与行情带跳色行指认)。
- **不动面(用户裁定)**:K 线窗标题保留合约;运维窗维持浮动弹窗形态
  (O1 内嵌页方案否决);「帮助」菜单保留。
- **钉**:s23 行选中(委托换选 + 成交独立槽);s17 改单击直达;快照重基线。
- **0.2.1 续(动作归行,用户裁定)**:行内任何一格单击/双击都触发该行动作
  (`click_cell` 标签矩形叠感应,外观不变);行情带盘口四列带方向
  (`import_tick`:买1 价/量 → 卖,卖1 价/量 → 买,价格列顺带填价,其余列
  方向/价格保持默认)。r8 钉逐列口径。

## BD-UI-32(0.2.2 字号与独立窗轮,2026-10-19;用户三点裁定)

- **R1 全局字号上调**(`apply_ui_style` 新口):body/button 16、small 14、
  heading 22、monospace 15(egui 缺省 13/12/18/12)——用户实看「整体字偏小」,
  对照 Python 版;render_body 顶部施加,真实 App 与 kittest 同源(快照重基线)。
- **R2/R3 查询合约窗 + K线窗独立 OS 窗**(`show_viewport_immediate`):
  原生最小化/最大化/关闭钮(此前 egui::Window 只有关闭钮,且被物理关在
  主窗矩形内不能拖出——用户对照 Python 独立窗形态指出);可拖到屏幕任意
  位置、独立任务栏条目。OS 关闭钮 = close_requested → 状态位翻负 → 父窗
  下一拍不再 show → eframe 销毁窗。Embedded 回退(后端不支持多窗:
  kittest/WEB)= 主窗内弹窗同一本体,测试断言面零改动。
- **不动面**:运维窗/CTP 窗维持主窗内弹窗(用户只点了这两个窗)。
- **选型**:immediate 而非 deferred——回调可捕获 `&UiShared`(UiShared 非
  Clone,deferred 需 Arc 化会动 render_body 签名 = 无谓破坏性);StationApp
  1s 节流重绘节奏下 immediate 的父子联动重绘代价可忽略。
- **0.2.3 勘误(关不掉根因,用户实看指出):** egui 销毁判据 = 子 viewport
  本拍未 used——0.2.2 调用点未按状态位守卫,每拍 show = 每拍 used = OS 窗
  永不销毁(X 翻位后又被 show 回来,等效 CancelClose),且窗在主窗一开就
  自己出现。修复 = show 整段包进 `contracts_window_open()` 守卫。测试面
  教训:Embedded 回退掩盖 Immediate 真窗面(21 绿 ≠ 真窗面正确),s24 注入
  recording renderer 补钉;复审纪律补条:新机制的每个调用点必须按状态位辞行。

## BD-UI-33(0.2.4 四窗一型独立化,2026-10-19;用户裁定)

- **运维窗 + CTP 网关窗也转独立 OS 窗**(「四窗皆可脱离主窗」):与查询合约/
  K线同形态同守卫;Embedded 回退维持主窗内弹窗(kittest 断言面与快照零改动)。
- **合约表吃满窗口:** 撤 max_height(260) 内嵌时代残留;动作行(计数 +
  订阅钮)前置,ScrollArea auto_shrink(false) 吃满剩余窗高——放大窗口 =
  表格变高(用户「放大窗口意义在哪里」的直接回应)。
- **`show_independent_window` 助手(四窗一型):** 守卫模式内建(0.2.3 教训
  固化成调用方纪律:位关不 show = GC 销毁);Embedded/Immediate 两面的
  "被关"统一以返回值出表,消除四份 viewport 样板重复。

## BD-UI-34(0.2.5 表头固定 + 主题 + K线最大化/ESC,2026-10-19;用户裁定)

- **表头固定(`pinned_table` 五表全上):** 官方 DIY 模式(scroll_area.rs:412
  注释「painting a sticky header on top of it」)——单 grid 实例(头行+体行)
  保列对齐,ScrollArea 之后以本拍捕获的表头格矩形重画表头带(背景盖行 +
  Label 重画;矩形已含滚动平移 → 横向跟随、纵向盖顶、零拍滞后;Label 无
  交互 sense → 点击穿透)。两轮机制实锤:①vertical 嵌 horizontal 高度塌缩
  (s3 底行钉 462<500 抓到);②同 id 双 grid 各从空白重建 State,save 交替
  request_repaint = 重绘环(kittest max_steps 爆)——两案皆弃,留 overlay 案。
- **主题四套(金融深色默认/egui深/egui浅/Qt灰白):** 菜单钮一键循环;
  Theme 枚举 + AtomicU8 运行期态(零配置口径);apply_ui_style_for 统一施加
  (主窗/独立子窗/kittest 三处同源)。语义色跨主题恒定。回答用户「颜色是
  默认还是自配」:此前 = egui 默认深色骨架 + 自配语义色(非全默认)。
- **K线窗:** maximized 打开即最大化(s25 钉 builder.maximized=Some(true));
  ESC 快捷关闭(body 返回 true → close_kline 同链;s25 钉 Embedded 面注入
  Event::Key;Immediate 面键按子窗焦点路由)。
- **行情带外层 ScrollArea 不可撤:** resizable 面板吃满前提(BD-UI-27),
  本轮实撤一次即 s3 底行钉实测塌到 462——已恢复并注释钉死。

## BD-UI-34 返修(0.2.6,2026-10-19;用户实看三报,全有机制实锤)

- **竖线(资金右):** 覆盖带 ui.put 视口外表头格 → 布局 min_rect 撑宽 →
  Frame 描边矩形撑到 x=1222(= 346.9 + 16 列总宽)。二分实锤:关覆盖带线
  消失;空内容时线跟内容宽走(1007)。修 = painter.text/rect_filled 纯绘制
  (零布局副作用)+ clip 裁越界。教训:覆盖/装饰一律走 painter,不得用布局
  调用(put/label)画视口外内容。
- **表头缝:** 带高须 = 表头格高 + item_spacing.y(3px),否则滚动行从间距带
  露出。
- **空表缩格:** 空态不得提前 return——pinned_table 照调(空 body),表头与
  吃满滚动区在场。s26 钉三表空态表头同在场。
- **group_frame_plain 使用纪律(0.2.6 钉死):** Frame::show 描边矩形按内容
  min_rect 收缩(实锤:label 内容时 gw 宽缩到 112)——只配 auto_shrink(false)
  填充物(pinned_table)使用;非表格格(资金/日志)必须走带填充 ScrollArea 的
  group_frame,否则 Frame 收缩、右边框落行中段。

## BD-UI-35(0.2.8,2026-10-19;用户实看三报,全机制实锤)

- 行情带状态行「tick N 格 / 最后更新」= 用户裁定撤除(考证:从未删过,
  0.2.0 撤的是题字)。s8 反向钉防回归。
- 表头缝硬根因 = clip_rect_margin(默认 3.0)外扩裁剪;覆盖带必须按
  inner_rect ± margin 起画。s27 滚动快照 + 像素扫描口径入 CHANGELOG。
- 空态提示一律在表头之下(grid body 首格),不得再置于 pinned_table 之前。
- 列头单源纪律扩散到全部四张格表(const HEADERS):本轮自审又抓到两处
  空态分支凭记忆写错——手写第二套表头 = 必错模式,单源是唯一防线。
- 快照回读串行化(SNAP_LOCK):本机 wgpu 并发回读会随机成组失败,测试闸
  对此环境敏感;新增快照测试必须走 snap() 不得裸调 harness.snapshot。

## BD-UI-36(0.2.9,2026-10-19;纸面网关迁出界面包)

- 用户裁定:纸面网关 = 网关,应与 ek-ctp 同级独立成包(Python 蓝图 PaperAccount
  即与 CTP/Xt/Qmt 同级独立 app;SimNow 登不上时切换的本地模拟流)。
  曾议落位:留 ek-ui(否决:界面包藏网关,无头测不到)/并入 ek-ctp(否决:污染
  纯 CTP 协议面)→ 裁定 = 独立 crate ek-paper。
- 语义零迁移实锤:旧 sim.rs 与新 matcher+plugin 非注释代码行 449↔449,
  Compare-Object 仅 4 处 Arc 写法面差异。
- 工厂表归一:sim 登记项上移 ek-station 0.1.8 default_kinds(无头/有头同一张表);
  ek-ui ui_kinds/pick_kinds 撤自建 sim 项防双份(注册名冲突 = add_plugin Duplicate)。
- 教训钉(注册表耦合顺序):改工厂表必须【先发布被依赖方】——station 0.1.8
  未发布前 ek-ui 测试 UnknownKind("sim")(本地改动对 registry 依赖不可见,
  这正是禁 path 耦合的代价与回报)。

## BD-UI-38(0.2.10,2026-10-19;纸面网关整体退场)

- 用户裁定(2026-10-19):纸面账户功能整体撤除,界面不再有 sim——
  **ctp/SimNow 两套前置(12 小时/7×24,选前置用 ek-ctp scripts/simnow-fronts.ps1
  -Probe)已满足当前需求**。BD-UI-37(sim 对外面对齐 CTP 的改造方案)作废,
  未执行半成品整堆丢弃(ek-paper 工作区回滚到 0.1.1 已发布态)。
- 退场涟漪(三包):ek-station 0.1.12 撤 ek-paper 依赖 + 工厂表 sim 项
  (pipeline_headless 改 dm/oms/ctp 装配 + CTP 回报行直发注入,账本/行情/因子
  钉数值零变);ek-ui 0.2.10 撤 ek-paper 依赖 + 下拉/兜底/钮/测试全清;
  ek-paper 0.1.x 档案原样留存(不删仓库不发版不 yank——yank 反咬已发布老版)。
- 界面口径:接口下拉 ∪ 集改 {ctp};parse_gateway 空兜底 sim→ctp;运维窗
  「流控查询」钮撤(UiCmd::QueryRate 变体同步撤——公共 API 减项);
  demo 窗 = dm/oms/ui(网关经运维窗激活);demo_tour 纸面段整段删(8 步重编)。
- 测试口径:ui_sim.rs 整删(9 钉);ui_converter_chain 网关位改测试本地
  "ctp" 受理夹具(受理形制忠实 ek-ctp:SUBMITTING 单上总线 + ok 回执 +
  `{gw}.` 全号;成交按 OnOrderTrade/OnRtnTrade 同构行直发)——本钉主体
  一直是 oms 五段管线,与网关撮合无涉,换顶替零减钉;kittest 夹具标签
  sim→ctp(五张快照重基线:s3/s10/s16/s21/s27,钮撤除 + 标签改名像素变);
  71→62 钉(减的 9 = ui_sim)。
- 纪律钉:历史条目(BD-UI-36、CHANGELOG 0.1.8/0.1.9 sim 入表等)是史实,
  不改写;退场只记新条目。

## BD-UI-39(多账号面:账户面板取代网关窗,规划 docs/ek-multi-account-plan.md)

> 本条取代的旧口径(史实保留不改写):BD-UI-24 的 `连接 CTP` → 硬编码
> `ctp.configure` / `ctp.subscribe`;BD-UI-27/33 的「CTP 网关配置窗」单账号窗体;
> BD-UI-38 的接口下拉 ∪ {ctp} 与 `parse_gateway` 空兜底 `"ctp"`。

- **菜单「网关」→「账户」**:旧单账号网关窗退役,收纳为账户面板的添加/编辑表单
  (9 输入位 + 密码掩码 + 收录勾选原样沿用,`ctp_grid` 未改字段)。旧窗的死面
  (`ctp_window` 状态位、`MENU_CTP`、`BTN_CTP_CONNECT`)一并删除,不留"供旧测试引用"。
- **账户清册在界面侧**(`UiShared.accounts`):行 = 派生注册名 + broker + 账号 +
  在场位 + 元账号标记 + 表单留档。派生名唯一入口 `ek_ctp::derived_gateway_name`
  (界面不自己拼字符串);密码只在内存,本轮不持久化(规划 §10.2)。
- **添加时序钉(规划 §5.A)**:一个钮两条 cmd —— `Start{factory:"ctp", name:<派生名>}`
  先装配,`CtpConfigure{gateway:<派生名>, form}` 后注入凭据(凭据不进 svctl.start 通道)。
- **单源纪律在"添加"这一刻定源(规划 §5.C)**:无元账号且无在场账户 → 新账户自动成为
  **行情元账号**(md 前置保留;清掉它等于这个账户自己也永远收不到 tick);已有元账号或
  已有在场账户 → 按纯 TD 装配(md 前置清空)。订阅两个入口(下单面板代码回车 /
  查询合约窗勾选)一律经 `subscribe_target` 路由到元账号,未指定就明确报错不静默发。
- **元账号连接后锁定(用户裁定「账户连接后不允许切换」)**:任何账户已装配即拒切换;
  换源流程 = 撤挂单 → 移除 → 改配置 → 重连 → 重放订阅(订阅集 `note_subscriptions`
  登记,元账号新装配后自动重放)。
- **移除守卫**:该账户有活跃委托 = 不给移除钮(只给"先撤单才能移除"提示);有持仓 =
  可移除但警告"柜台侧不受影响,重加后由轮转查询重建视图";两步确认沿用 `confirm_step`。
- **网关名回落只剩一个阶梯**:`default_gateway()` = 上次手动选择 → 首个已装配 → 首个;
  一个账户都没有 = 禁用「委托」钮并给提示(旧"空 → 字面量 ctp"兜底退役,B4)。
  撤单三处(行内钮 / 全撤 / 行双击)一律随该单自己的 `gateway_name`。
- **诚实标注钉**:账户表状态列写「已装配 / 未装配」不写「已连接」——界面只能从
  `svctl.list` 看见实例在场,登录成败 ek-ctp 无状态口可查(开放项)。
  **(0.2.19 已接上:`{gw}.status` 落地,状态列改两段口径「已装配·<登录态>」;
  「不写已连接」这条口径不变,见文末 0.2.19 节。)**
- **测试口径**:s13 重写(添加账户 → 派生名 + 两条 cmd + 首个账户自动定源 +
  第二账户 md 清空不被抢源)、s17 菜单改「账户」、s18/s20 订阅路由到元账号、
  新增 s28(元账号锁定 + 挂单移除守卫 + 守卫解除);五张快照重基线
  (s3/s10/s16/s21/s27,菜单标签与 tick 列名「接口」→「行情源」像素变)。

## 0.2.19(接上网关状态口 + 『断开』钮;规划 docs/ek-ui-account-tabs-plan.md P3,2026-10-22)

事实源换成了 ek-ctp 0.9.0 的两个服务口(`{gw}.status` / `{gw}.disconnect`)。界面侧的六条口径:

- **D-UI-STATUS-1 状态列两段,不合并**:装配位(`svctl.list` 在场)与登录态(`{gw}.status`
  读数)是两件事,拼在一栏显示(`已装配·在线` / `·掉线` / `·登录失败` / `·查询断供` /
  `·已断开` / `·登录态未知`)但**不合成一个布尔**。0.2.18 的「已装配·登录态未知」保留为
  读数缺失时的显式档位 —— 缺口从「不可见」变成「本轮还没问到」,两者必须长得不一样。
- **D-UI-STATUS-2 只认 `logged_in`,不认 `handle_up`**:网关的「句柄已建 / registerFront 已发」
  在掉线时仍是真(ek-ctp DESIGN D-STATUS ①)。界面照抄它 = 替掉线的账户谎称在线(s48 读数二钉)。
  同理 **"从未回报过"不当"0 秒前回报过"用**:`td_last_account_ms = None` 是"没有这件事",
  不是"很久没做"。
- **D-UI-STATUS-3 阈值住在界面**:「查询断供」= 连续被拒达 `QUERY_REJECT_STREAK_LIMIT`(3)
  **或** 资金回报超 `ACCOUNT_STALE_SECS`(30 秒)。网关只给原料(计数与时刻),"几次/多久算断供"
  是界面口径 —— 它才知道自己多久刷一次、多久问一次。覆盖位 `set_status_thresholds` 已开,
  配置文件面还没定(规划 Q10 只说"进配置",ek-ui 现在零配置)。
- **D-UI-STATUS-4 轮询只问"已装配且在场"的行**:每 `STATUS_POLL_TICKS = 2` 拍问一次。
  实例被摘除后清册行还在,照问不误就是每次等满 2 秒超时、按账户把 eTimer 刷新循环拖住(c5 钉)。
  状态口在 ek-ctp 侧只读、不写日志,所以这条轮询不吃 eLog 环(512 行环保护不变)。
  **问不到就退回「未知」并写明原因**,上一轮的「在线」不得留档(s50)。
- **D-UI-DISCONNECT-1 断开 = 停会话,移除 = 停实例,两条口不共用**(规划裁定 1):
  『断开』发 `UiCmd::Disconnect` → `{gw}.disconnect`;『移除』发 `UiCmd::Remove` → `svctl.remove`。
  断开后装配态不变、徽标「已断开」、恢复入口仍是『连接』(插件侧走「已在场 → 重投凭据」。
  **同一道活跃委托守卫两者共用**:会话一停就撤不了单也不再回报,与『移除』要挡的是同一个
  观测黑洞(规划 §5.B),出口也同一个(『先撤 N 笔』)—— 这是执行自查补上的一条(s54)。
- **D-UI-STATUS-5 实例不在场即作废读数**:`sync_accounts_connected` 把行从「已装配」打回
  「未装配」时,登录态与细节一并清零;渲染侧只给已装配的行印状态细节(s55)。
  留着就是替一个已经不存在的会话说话。
- **纯行情账户看 MD 半边**:判据是"该行 td 前置留档是否为空"。TD 半边对它是全假的,
  照抄只会得到「未知」——那是假信息(s53)。
- **测试口径**:kittest 45 → 53 钉(s48 三档读数与旧读数不留档、s49 断供两判据与阈值覆盖、
  s50 问不到退回、s51 『断开』入列与徽标翻转、s52 未装配无『断开』钮、s53 纯行情看 MD、
  s54 断开共用活跃委托守卫、s55 摘除即作废读数);s37 口径翻转;总线级新增 c4/c5
  (真起一个 ek-ctp 实例:服务目录含 `.status`/`.disconnect`、ask 真拿到 `GatewayStatus`
  —— 读数里没有「问不到」就是两份 ek-ctp 没静默断的证据);s39/s40/s41 快照重基线。

## 0.2.20(三条开放口径落进界面 + 接 ek-ctp 0.9.1 分项;2026-10-22)

- **D-UI-F4-SCOPE 勾选的作用范围写死成一句话,两处同源**:勾选只管**这个网关自己**收录并上抛的
  合约;公用合约表 = 在场各网关的并集(规划 D3「合约与行情公用」的直接推论);改勾选须
  『连接/重试』才下发。为什么必须写在界面上:实连 S14b 里账户 B 改勾选「期货+期权」后公用表
  仍 852 条、期权 0 条,而界面没有任何地方区分「没生效」与「网关没查到」—— 让用户猜就是
  把口径缺陷转嫁给用户。**同一口径不得只写在一处**(面板底部 + 配置弹窗,s57 数到 2 处)。
- **D-UI-Q17 运维窗对账户清册实例只列不操作**:名字在清册里 → 那一格改写成「起停归『账户』面板
  (带撤单守卫)」,不给『移除』也不给『重注入』(后者对命名实例本来就是 unknown factory)。
  理由不是"整洁",是**守卫可绕过**:账户面板的『移除』带活跃委托闸 + 二次确认 + 持仓提示,
  运维窗那条是同一动作的无守卫副本,两条路并存等于守卫形同虚设。**清册外的 `ctp_*` 实例仍留
  这条出口**(手工起的 / 旧流程残留没有别的退路)—— 边界收口不等于把门焊死。实例仍列在表里:
  砍的是操作,不是运维可见性(s56)。
- **D-UI-LOG-WINDOW 日志窗自报是窗口不是历史**:溢出计数原来只有数字;现在同一行接上去处
  (「登录/掉线/查询被拒看状态列与状态细节,别回扫这里」)。依据是巡回 G1 的实连证据 ——
  90 秒没抓到「交易服务器登录成功」行而资金行下一拍就刷新,**登录其实成功,是 512 行环被
  状态推送流挤满**。取证的事实源自此是网关状态口(0.2.19 起),不是日志回扫(s59)。
- **D-UI-STATUS-6 分项点名但不另开一行**:ek-ctp 0.9.1 的 `td_instrument_rejects` 接进状态细节
  句尾(「查询连续被拒 3 次;合约查询被拒 2 次」),同一件事不重复点名,分项为 0 不加句子。
  总数与连续串**不分路**是对的(消费侧要的是「查询这条路连续被拒几次」),但合约一条没到手时
  下一笔单会被 ContractGate 以 `contract:unknown` 拒掉 —— 状态列只写「在线」就是假信息(s58)。
- **测试口径**:kittest 53 → 57 钉(s56 运维窗收口且工厂插件照旧、s57 口径两处同源、
  s58 分项三段、s59 日志窗口径);s39/s40/s41 快照重基线(面板底部多一行口径句);
  `.gitignore` 补 `**/snapshots/**/*.old.png`。

## 0.2.21(第三条断供判据:「从未到手」也要超时;2026-10-22,依赖不变)

- **D-UI-STATUS-7 「从未发生」不能只是"没有这句话",它自己也要有超时位**:0.2.19 立「`None` 不当 0 用」
  时,只把它用在"距最后一次回报多久"这一判据的**前置条件**上(`Some` 才谈超时)。实连 S15f 第一轮
  读数照出洞:F2 账户 `logged_in=true`、`td_query_rejects=0`、`td_last_account_ms=None` 持续 390 秒
  不变 —— 于是界面永远是「已装配·在线 + 资金回报尚未到手」。**徽标是绿的,查询却彻底停摆**,
  这就是 F2 在界面上的形状。
- **修法与边界**:登录着的分支第三条判据 = 距 `td_last_login_ok_ms` 超过同一个 `ACCOUNT_STALE_SECS`
  且资金时刻仍 `None` → `已装配·查询断供` + 「资金回报从未到手(登录已 Ns)」。刚登录几秒不算
  (轮转第一拍本就要 2~6 秒);真到手即回在线。**不新建第二个阈值**:"多久算等太久"在两条判据里
  必须是同一个数,否则同一件事有两种松紧。
- **为什么修在 ek-ui 而不是 ek-ctp**:判据原料(登录时刻 + 读数时刻)网关早就给了,缺的是消费侧
  怎么用 `None`。网关侧加"资金超时"位就是把界面口径往上游搬(D-UI-STATUS-3 的反面)。
- **测试口径**:kittest 57 → 58 钉(`s60` 三段:5 秒 = 在线带话 / 40 秒从未到手 = 断供带时长 / 到手即回在线)。

## BD-UI-40(0.2.26,2026-10-22;第五套主题「Qt深色」= Python 版黑色主题复刻)

- **依据是读出来的,不是印象**:Python 版 `enkeep_trade/enkeep_trade/trader/ui/qt.py:102`
  `qapp.setStyleSheet(qdarkstyle.load_stylesheet(qt_api="pyside6"))`;本机已装
  **QDarkStyle 3.2.3**(`.venv/Lib/site-packages/qdarkstyle`)。色板取自 `dark/palette.py` +
  `colorsystem.py` 原值,规格取自编译产物 `dark/darkstyle.qss`(2216 行实测:
  `border-radius: 4px` 出现 48 次且是全主题唯一圆角值;描边一律 1px;padding 以 2px 为主)。
- **主题从四套变五套**,菜单钮一键循环加一站:`Qt灰白 → Qt深色 → 金融深色`。
  **缺省仍是金融深色** —— 加主题不换缺省,现有 8 张快照一张没动(git 里只有 s42 是新增)。
- **`Theme as u8` 是 `set_theme`/`theme()` 之间的存储形式**,新变体必须留在枚举末尾、
  且 `theme()` 的 match 必须加 `4 =>` 臂。**漏臂是这条链上最阴的一种错**:编译器不报、
  `set_theme` 照存、读回来落进 `_ =>` 解回金融深色 —— 钮上文案换了而画面不换。挡它的是
  `r9c`(五个判别值逐个钉死 + 每个定点值往返一遍)。
- **映射口径(逐条对过 qss)**:
  - `window_fill = panel_fill = extreme_bg_color = #19232D`(Gray.B10)。qdarkstyle 所有容器
    同底、靠 1px `#455364` 描边区分 —— 三个字段同值不是偷懒,是照抄。
  - 按钮三态 = `#455364 → hover #54687A → pressed/checked #60798B`,且 qss 里
    `QPushButton { border: none }` → `bg_stroke` 与 `bg_fill` 取同色(画出来就是无边)。
  - `expansion` 一律 0.0:qss 的 hover 只换色不涨块,egui 缺省 hover/active 涨 1px。
  - `fg_stroke` 宽度一律 1.0:Qt 不在 hover/pressed 加重文字,egui 缺省 1.5/2.0。
  - `selection.bg_fill = #346792`(Blue.B40):qss 里 `QWidget::item:selected` 与
    `QLineEdit:selected` 同值。**`selection.stroke.color` 被 egui 当选中项文字色用**
    (style.rs:374)→ 取 TEXT1 `#DFE1E2`;照抄 qss 的 focus 边框 `#1A72BB` 会让选中 tab 文字变蓝。
  - `weak_text_color = Some(#ACB1B6)`(Gray.B90)、`hyperlink_color = #259AE9`(Blue.B70)、
    `faint_bg_color = #293544`(Gray.B20)、圆角全态 `CornerRadius::same(4)`。
  - `interact_cursor = Some(PointingHand)`:Qt 按钮是手型光标,egui 缺省是浏览器口径。
- **已知偏离(做不到 / 决定不做,逐条给理由)**:
  1. **选中 tab 那条 3px `#259AE9` 下划线**(qdarkstyle 的签名样式)—— egui `Visuals` 没有这个
     概念,`selectable_label` 只能整块填充。做它要改 `render.rs` 里 8 处手绘,不在本轮范围。
     本轮取「整块强调色」代替「灰蓝底 + 亮蓝下划线」。
  2. **字号不动**:Python 版是全局 `QFont.pointSize(11)`(96dpi ≈14.7px),我们 body 16px。
     字号在 `apply_ui_style_for` 里是跨主题的全局口径、不是主题属性,改它牵动其余四套与 8 张快照。
  3. **禁用文本色 `#788D9C` 做不到**:egui 只调 `disabled_alpha`,不能指定具体灰蓝 → 保持 0.5
     (那档亮度约 0.55,视觉近似)。
  4. **菜单底色 `#37414F` 做不到**:egui 0.33 的 menu/popup 背景跟 `window_fill`,无独立字段。
  5. **不移植图标资源**(qss 的 `rc/*.png` 箭头、树形 branch、窗口关闭钮、14×14 复选框图标)
     —— egui 自绘,只映射颜色不映射形状。
  6. QGroupBox 的 1px 描边 + 粗体标题、表格 gridline:egui 无对应物,靠
     `noninteractive.bg_stroke = #455364` 近似。
- **有意不设的字段(防「多余」)**:`text_edit_bg_color`(缺省跟随 `extreme_bg_color`,同值设两遍
  是重复)、`code_bg_color`(本 UI 的 `ui.monospace` 只换字体不带背景)、`warn/error_fg_color`
  (语义色在渲染侧直绘、跨主题恒定 —— D-UI-34 那条口径不变)、slider/progress 相关(本 UI
  0 处 slider、0 处进度条)。
- **测试口径**:ui_state 13 → 16 钉(`r9` 循环改五站 + `r9b` 色值钉 + `r9c` 编码钉 +
  `r9d` 反证钉),kittest 58 → 60 钉(`s42_qdark_theme_full_panel` 快照 + `s61` **真实手势**钉:
  不碰 `set_theme`,只点菜单钮,连点四下从缺省走到 Qt深色,每档都判「`shared.theme()` 换到位 +
  钮文案跟着换 + 画面底色真的换」,再点一下判环闭合回缺省;`s42` 夹具与 `s21` 同源 ——
  同一副内容换主题,快照差异只剩主题本身)。全绿 = 104 钉。
- **快照不只用眼睛看**:`target/theme-pixels.ps1` 把 s42 的 110 万像素数成直方图 ——
  面板底 `#19232D` 占 **93.17%**、`#455364` 2.09%、`#293544` 2.17%、正文 `#DFE1E2` 2125 px、
  选中块 `#346792` 748 px;反证两条同样硬:egui 原生深色底 `#1B1B1B` **0 px**、
  金融深色底 `#18202C` **0 px**(出现即说明主题没切过去)。`#54687A`/`#60798B`/`#259AE9`
  在静态快照里 0 px 是**预期**(静态图里没有 hover/pressed 控件、界面里没有超链接),
  那三支只在 `r9b` 的 Visuals 层钉住。
- **实连巡回取证(真窗口,不是 kittest)**:`examples/opening_multi_tour.rs` 加了一个
  `EK_TOUR_THEME` 开关(`qdark | finance | egui-dark | egui-light | qt-classic`,缺省不动;
  `examples/` 不在 `cargo package` 清单里 → 不进发布包、不动 crate API)。2026-10-21 17:3x
  用 7×24 前置(`EK_CTP_TD/MD` 指到 `EK_CTP_724_TD/MD`)跑一趟 `EK_TOUR_THEME=qdark`:
  控制台首行「【开盘演示】主题 = Qt深色」;元账号 `ctp_9999_278153` 登录 = true、纯 TD
  `ctp_9999_220380` 登录 = false(该账号在 7×24 前置未注册 —— 环境侧,与本轮改动无关);
  交易状态推送流 129 行。窗口截图 `target/gui-qdark.png`(1416×939)同样过像素直方图:
  `#19232D` 占 **83.96%**(余下主要是 OS 标题栏与边框)、`#293544` 3.65%、`#455364` 1.82%、
  正文 `#DFE1E2` 2111 px、选中 tab `#346792` 707 px,`#1B1B1B` 与 `#18202C` 仍是 0。
  截图里菜单钮读作「主题:Qt深色」,委托/成交两表是柜台真数据(6 笔委托 / 2 笔成交),
  语义色没漂(LONG 红 / SHORT 绿)。

## BD-UI-41(0.2.27,2026-10-22;对照 Python 版逐项核出来的界面面:表头带 / 网格线 / 日志表格化 / 页签头与下划线)

起因是用户实看 ek-ui 真窗口后点出的五处,并要求「去 Python 版本再看,逐个对比,肯定还有别的细节」。
核对方法:两边都读代码,不凭印象 —— Python 侧读 `enkeep_trade/enkeep_trade/trader/ui/mainwindow.py`
(布局)+ `widget.py`(表格与监控控件)+ 已装 qdarkstyle 3.2.3 的 `dark/darkstyle.qss`(2216 行,样式)。

### 逐条核对结果(12 条,两边都有行号)

| # | 项目 | Python 侧事实(行号) | 我们改前 | 处置 |
|---|---|---|---|---|
| 1 | 表头带底色 | qss:1946-1970 `QHeaderView::section { background #455364; color #DFE1E2; font-size 13px }` | 覆盖带填 `panel_fill` = 表体同色 → 等于没画 | **补**(本轮核心) |
| 2 | 表格外框 | qss:514-519 / 1875-1884 `border: 1px solid #455364` | 已有(`Frame::group` 描边 = `bg_stroke` `#455364`) | 不动 ✓ |
| 3 | 网格线 | 交易表 `widget.py:367 setShowGrid(False)`(注释「隐藏网格线」);**只有**合约表 `2423` / `2621` 没关 | 全无网格 | **只给合约表补** |
| 4 | 斑马纹 | `widget.py:359 setAlternatingRowColors(True)`,但 qss:515/1879 `alternate-background-color` = `#19232D` = 表体底 → 看不见 | 我们 `striped(true)` 硬开,奇行 `#293544`(实连截图里占 10.72%) | **收掉**,改跟 `visuals.striped` |
| 5 | 日志 | `LogMonitor`(`widget.py:1167-1180`)= 带「时间/信息/接口」表头的 QTableWidget,跟 Dock 伸展 | monospace 行 + `max_height(180.0)` 硬顶 | **换成 `pinned_table`** |
| 6 | 每格都有 tab 头 | 四个区全是 QTabWidget(`mainwindow.py:106/126/168/184`),单 tab 的「成交」「持仓」也有头 | 成交/持仓只是 `ui.label` | **补单 tab** |
| 7 | 选中 tab 下划线 | qss:1592 `:selected { background #54687A; border-bottom: 3px solid #259AE9 }` | 无(0.2.26 记为「做不到」) | **补** —— 那条已知偏离作废 |
| 8 | 未选中 tab 头底 | qss:1579 `QTabBar::tab:top { background #455364 }` + 1559 `!selected { border-bottom 2px }` | 无 | **补** |
| 9 | 服务目录表头 | 合约/账户类表都有 `setHorizontalHeaderLabels` | `svc_grid` 六列纯数据无头 | **补**(用户点名) |
| 10 | 表头字号方向 | 表头 13px < 正文 ≈14.7px(`QFont pointSize 11`,`qt.py:106-107`) | 表头 = 正文 = 16px(方向反了) | **表头改 Small 14** |
| 11 | 单元格 padding | `widget.py:369 item { padding: 4px 8px }` | `item_spacing (8,4)` | 相近,**不动** |
| 12 | 列集合 | 委托 16 列 / 行情按所分表 / 成交 12 列 | 委托 17(多「操作」)、行情 14 用 tab 分所 + 「今开」、成交号放第一列 | **有意不动**(功能差异,不是漏) |

### 落法与理由

- **`Chrome` 取色层**:qdarkstyle 有「表头带」和「选中 tab 下划线」两个概念,egui `Visuals`
  里没有对应字段(它只有 `selection.bg_fill` 一根)。所以这层只能在渲染侧直绘。
  取色**不新造颜色**,全部从各主题已声明的字段派生:`line = noninteractive.bg_stroke.color`、
  `header_text = noninteractive.fg_stroke.color`、`tab_underline = hyperlink_color`、
  `tab_fill`(Qt深色)= `hovered.bg_fill`。Qt深色那一列取出来正是 qss 原值
  (`#455364` / `#DFE1E2` / `#54687A` / `#259AE9`),其余四套取自己的色板 —— 五套一起改,
  因为只改 Qt深色等于没改用户日常看的那一套。
- **为什么文字必须重画**:epaint 0.33 没有 z-order(全 crate 搜过:无 `ZOrder` / `z_order` /
  `Shape::bg_fill`),形状按插入顺序画 → 后画的带底会盖住先画的表头文字。所以
  「盖底 → 画线 → 重画文字」是固定套路,`pinned_table` 覆盖带、`header_band`、`tab_label` 三处同一个理。
- **`header_band` 而不是 `pinned_table`**:账户清单 / 运维插件表 / 服务目录在**自动尺寸浮动窗**里。
  0.2.17 实锤(s39):`pinned_table` = `auto_shrink(false)` 吃满整块可用高 + 覆盖带,
  在这种窗里凑不出高度 → 表行与其后的口径句互相压字。这三张表行数个位数,不需要固定表头滚动面,
  只要那根带。
- **网格线的画法**:`pinned_table_grid` 从 grid 实测的**列边界矩形**(表头格)推列线 x
  (`h.max.x + item_spacing.x * 0.5`),从内容矩形 + 行数等分推行线 y。前提:每格都是 label
  → 行高一致(合约表正是如此)。所以网格线只给合约表,交易表不给(那边关了网格)。
- **日志时间戳口径**:`LogRing::push` 入环打 `chrono::Utc::now().with_timezone(+08)` 的
  `%H:%M:%S`,与 `fmt_dt` 同一口径 —— `beijing_tz` 因此从 render 提到 state,一份实现两处用。
  本机不是 +08 时口径错 = 日志与成交表差几小时,交易员对不上账(钉:
  `log_entries_stamp_beijing_wall_clock`)。
- **`snapshot()` 门面不动**:`LogRing` 内部从 `VecDeque<String>` 换成 `VecDeque<LogEntry>`,
  但 `snapshot()` 仍返回正文串 —— 30 处既有调用点(测试 / 巡回取证 / demo)一行没改。
  渲染侧另开 `entries()` / `logs_entries()`。两个口各有真实消费者,不是重复。

### 已知偏离(记死,不藏)

1. **日志只有两列**(时间/信息),Python 有第三列「接口」:我们的 `log_line` 调用点不带网关,
   正文自带 `[ops]` / `[订阅]` 这类前缀。硬造一列空列不如不造。
2. **日志不自动跟到最后一行**(`stick_to_bottom` 撤除):Python 的 QTableWidget 也不自动滚到底。
3. **斑马纹**:Python 的意图是开(`setAlternatingRowColors(True)`),但 qss 让它不可见。
   我们跟「不可见」这一结果。**这一条我先前给过相反判断**(一度主张保留斑马帮读 17 列宽表),
   最终按「与 Python 一致」定;若日后要开,应开在主题层(`visuals.striped`)而不是表里硬开。
4. **字号**:全局 Body 16 / Small 14 vs Python 全局 `pointSize 11` ≈14.7px。字号是跨主题全局口径,
   本轮只把「表头比正文小」这个**方向**修对,没有动全局字号。
5. **列集合**(表 12 行):功能差异不是漏。

### 审计(遗漏 / 错误 / 重复 / 多余 / 逻辑)

- **改了什么、删了什么,逐条可查**:删除的只有 `group_frame`(四格全走 `group_frame_plain` 后无使用者)
  和 render 侧那份 `beijing_tz`(搬到 state,render 改 import)。**没有删任何功能面**:
  日志钮、溢出提示句、清空钮、`snapshot()` 门面、`log_grid` 之外的所有表容器都在。
- **发现并修掉的自己的错**(都是本轮内自查抓到,不是门禁抓的):
  ① 合约表两处调用点我先把**空态分支**换成了 `pinned_table_grid(…, rows.len(), …)`(空表画网格),
  下一眼读回时看到并改回;② `Chrome` 字段用了裸 `Color32` —— `use egui::Color32` 是
  `theme_visuals` 函数内的局部 import,结构体在函数外看不见,编译报 E0425;
  ③ `Painter::line` 在 0.33 收 `Vec<Pos2>` 不收元组 → 改 `vline` / `hline`;
  ④ `Grid::show` 返回 `InnerResponse`,`gr.rect` 不存在 → `gr.response.rect`;
  ⑤ 新测试里「时间」「服务」「实例」都不是唯一标签(行情带也有「时间」列、运维窗也有「服务」标题、
  服务目录有三对「实例」头)→ 改 `query_all_by_label` 数个数,顺带把断言变强。
- **重复**:一度想给日志表加「时间列 + 正文列 + 接口列」三列,与「调用点不带网关」冲突 → 砍成两列。
  `snap` / `snap_budget` 两个快照入口是**必要**的两条:一条 0 容差,一条带墙钟预算,共用同一把锁。
- **多余**:`group_frame` 删掉;`Chrome` 里一度准备放 `tab_head_stroke` 与 `tab_underline_width`
  两个字段,前者与 `line` 同值、后者只有 2/3 两个常量 → 都不进结构体,写在使用处。
- **逻辑问题(本轮最容易再犯的一种)**:带墙钟时间列的快照天然不确定。第一反应是「调容差」,
  真正的处理是把**结构面**钉在 AccessKit 标签上(确定性),把**像素图**降级成带预算的辅助证据,
  并在 `snap_budget` 的注释里写清为什么这三张要预算。
- **没动的**(有意):`snapshot_lock()` 旧调用形式(9 处)只在新钉里换成 `snap_guard` ——
  改全部旧站点属于另一轮清理,本轮不夹带。

### 证据

- 门禁:`scripts/gate.ps1` 全绿(fmt / test 111 钉 / clippy `-D warnings` 0 / doc 0 / check-package strict)。
- 快照:9 张重生成 + 新增 `s63_logs_table_headers`、`s65_ops_service_headers`;
  肉眼核过 `s42`(Qt深色)与 `s21`(金融深色):六张表都有表头带,委托/未成交、资金/日志、
  行情带交易所、成交、持仓五处都有 tab 头,选中 tab 有下划线,画面无斑马。
- 像素直方图(`target/theme-pixels.ps1`,本轮加两条硬检查):`#259AE9 ≥ 1`(选中 tab 下划线,
  改前 0)与 `#54687A ≥ 200`(选中 tab 底,改前只在 hover 时出现 = 0)。
- 真窗口:`EK_TOUR_THEME=qdark` 实连 7×24 前置一趟,截图过同一套像素检查(见本轮报告)。