Skip to content

Commit a8ac26b

Browse files
committed
docs(spec): capture model form state bug lessons
1 parent c68f7fa commit a8ac26b

File tree

11 files changed

+499
-0
lines changed

11 files changed

+499
-0
lines changed

.trellis/spec/frontend/component-guidelines.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,40 @@ function requestConfirmDialog({ title, message, confirmText, cancelText, variant
8181

8282
---
8383

84+
## Form-Heavy Admin UI Pattern
85+
86+
模型管理、节点控制、技能配置这类后台页面属于 **data-dense admin UI**,应遵循下面的构建模式:
87+
88+
### 1. Visible labels first
89+
90+
- 输入框必须有可见 label 或 field title
91+
- placeholder 只能作为示例,不能承担标签职责
92+
- 技术字段(model id、provider key、base URL、路径、别名)尤其不能只靠 placeholder
93+
94+
### 2. Split values from policies
95+
96+
- 原始值输入(字符串、数字、URL)和策略选项(allow/default/enabled)不要混在同一视觉语言里
97+
- 推荐结构:
98+
- 上层:字段输入
99+
- 下层:checkbox / radio 等策略项
100+
- 不要为了“更酷”把 checkbox / radio 包装成一次性自定义发光 pill,除非仓库里已有共享控件可以复用
101+
102+
### 3. State expression should be explicit
103+
104+
- 普通内容标签(如模型名、provider 名)默认保持中性
105+
- 状态要通过文案、icon、badge、helper text 来表达
106+
- 不要把“可用 / 默认 / 选中”直接变成内容本身的语义色文本
107+
108+
### 4. Inline helper copy
109+
110+
- 当一个控件会影响配置写入行为时,必须在附近提供简短说明
111+
- 例如:
112+
- “勾选后会写入 allowlist”
113+
- “保存后会更新默认模型”
114+
- 这类说明比把控件做成复杂视觉形态更重要
115+
116+
---
117+
84118
## 样式约定
85119

86120
- **CSS 自定义属性体系**:所有颜色、阴影、动效时长等通过 `:root` 变量定义(`public/styles.css`
@@ -99,6 +133,7 @@ function requestConfirmDialog({ title, message, confirmText, cancelText, variant
99133
- **`aria-hidden`**:弹窗开关时同步设置
100134
- **`prefers-reduced-motion`**:检测并禁用 spotlight 动画(约第 79 行)
101135
- **键盘导航**:Enter 键提交表单、Escape 键关闭弹窗
136+
- **表单控件可访问名**:checkbox / radio / icon-only 按钮必须有明确 label 或 `aria-label`
102137

103138
---
104139

.trellis/spec/frontend/design-system.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,22 @@ Core principles already landed in code:
1515
- motion durations centralized in tokens
1616
- spotlight / ambient effects must degrade cleanly under `prefers-reduced-motion`
1717
- shared button / badge / panel classes should be reused before adding new component-specific styling
18+
- admin / control surfaces are **data-dense first**: clarity, trust, and readable form structure beat decorative flourish
19+
20+
### Product Fit: Admin / Gateway Console
21+
22+
For this project, external design research best matches a dark enterprise control surface:
23+
24+
- “enterprise gateway” / admin dashboard visual language
25+
- conservative accent usage
26+
- high-contrast dark mode
27+
- technical typography for ids / model refs / machine-readable strings
28+
29+
This means new UI should feel like an operator console, not a marketing page:
30+
31+
- prefer calm, conservative surfaces over playful highlight colors
32+
- reserve semantic colors for explicit status signals, not ordinary content labels
33+
- treat forms and control panels as operational UI, not decorative cards
1834

1935
---
2036

@@ -72,6 +88,14 @@ Use shared button classes for interaction states. Only add local styles for spac
7288
- `.status-badge` and its tone variants are the default status capsule.
7389
- Use semantic tones (`ok`, `warn`, `bad`, `accent`, `dynamic`) consistently across views.
7490
- **Warning**: light theme keeps a broad base `.status-badge` restyle. Existing tones used by the app have dedicated overrides, but any newly introduced badge tone must add its own light-theme override instead of assuming the base badge style will preserve meaning.
91+
- For admin/data chips (model ids, provider names, route labels), default to the neutral chip language first.
92+
- Do **not** recolor normal text/chips to green/yellow/red just because the underlying item is “available”, “default”, or “selected”.
93+
- If a state matters, expose it through:
94+
- a nearby `.status-badge`
95+
- helper copy
96+
- explicit label text / icon
97+
- layout grouping
98+
rather than colorizing the content token itself.
7599

76100
### Spotlight-enabled cards
77101
- Add `data-spotlight` to interactive elevated surfaces that should receive pointer-driven light.
@@ -80,6 +104,12 @@ Use shared button classes for interaction states. Only add local styles for spac
80104
### Form / modal system
81105
- Reuse the existing modal shell, field spacing, and focus behaviors.
82106
- Inputs should inherit token-based backgrounds/borders and rely on the global `:focus-visible` outline instead of custom ad-hoc rings.
107+
- In form-heavy admin modals, use **visible field labels** and helper text before inventing new control metaphors.
108+
- One-off custom switches / glowing pills / chip-toggles should be avoided unless there is already a shared primitive in the repo.
109+
- For policy-like choices (“enabled”, “default”, “allow”), prefer native checkbox / radio inputs with clear labels unless a shared existing control family is already established.
110+
- Dense forms should be structured in two layers:
111+
- field rows for raw values
112+
- a separate options / policy row for toggles and secondary choices
83113

84114
---
85115

@@ -110,6 +140,7 @@ When adding a new animation, wire it through the shared motion tokens or the sam
110140
### Focus and keyboard
111141
- Rely on the global `:focus-visible` outline (`public/styles.css:96-106`) unless a component has a stronger documented reason.
112142
- Modal and drawer interactions must keep keyboard flow intact; visual polish must not break Tab / Escape behavior.
143+
- Inputs, radios, and checkboxes in admin forms should remain obviously interactive in both themes without requiring hover to reveal affordance.
113144

114145
### Theme switching
115146
- Dark mode is the baseline in `:root`.
@@ -119,6 +150,15 @@ When adding a new animation, wire it through the shared motion tokens or the sam
119150
### Color independence
120151
- Accent color is emphasis, not the only source of meaning.
121152
- Pair state colors with iconography, label text, or placement when the state matters.
153+
- If a user cannot distinguish green from neutral, or if the UI is viewed in grayscale, critical state should still be understandable.
154+
155+
### Admin Form Rules
156+
157+
- Placeholder text is an example, not the primary label.
158+
- Technical inputs (model ids, provider keys, API endpoints, aliases) should keep labels visible at all times.
159+
- Required-ness should be explicit when applicable.
160+
- Submission feedback must have a visible loading / success / error state.
161+
- Inline validation and field-local errors are preferred over a single generic error area at the top.
122162

123163
---
124164

@@ -175,6 +215,8 @@ When adding a new animation, wire it through the shared motion tokens or the sam
175215

176216
- [ ] 是否优先复用现有 token,而不是硬编码颜色 / 阴影 / easing?
177217
- [ ] 是否优先复用 `.glass-panel` / `.btn-*` / `.status-badge` 等共享类族?
218+
- [ ] 是否把“后台控制台的值展示”和“状态表达”分开了,而不是给普通内容直接染语义色?
219+
- [ ] 表单是否优先使用可见 label + 原生 checkbox/radio,而不是一次性私造控件?
178220
- [ ] 新动效是否接入了共享 motion token?
179221
- [ ] `prefers-reduced-motion` 下是否仍然可用?
180222
- [ ] light / dark 主题下都是否可读?

.trellis/spec/frontend/quality-guidelines.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@ function stopOverviewTimers() {
7070
- 设置 `aria-hidden` 属性
7171
- 支持 Escape 键关闭
7272

73+
### 6. 表单质量要求
74+
75+
- 每个输入控件都需要可见 label;placeholder 不能代替 label
76+
- 错误信息应尽量贴近对应字段,并在需要时用 `aria-live` / `role="alert"` 让读屏可感知
77+
- 不要只靠颜色表达状态或错误
78+
- 技术输入不要阻止 paste
79+
- 技术输入应根据场景设置合适的 `type``autocomplete``spellcheck`
80+
- 例如 URL 用 `type="text"``type="url"` 时要确保可读
81+
- 代码 / 模型 ID / provider key 可考虑 `spellcheck="false"`
82+
7383
---
7484

7585
## Testing Requirements
@@ -90,4 +100,7 @@ function stopOverviewTimers() {
90100
- [ ] UI 文本是否使用中文?
91101
- [ ] CSS 是否使用已有的自定义属性(`--accent``--surface` 等)而非硬编码颜色?
92102
- [ ] 新增元素是否遵循 `.glass-panel` / `.stat-card` 等已有设计模式?
103+
- [ ] 表单是否有可见 label,而不是只靠 placeholder?
104+
- [ ] 状态和错误是否不是只靠颜色表达?
105+
- [ ] checkbox / radio / icon-only 按钮是否有清晰可访问名称?
93106
- [ ] 是否尊重 `prefers-reduced-motion` 设置?

.trellis/spec/frontend/state-management.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ const state = {
2121
currentView: "overview", // 当前激活的导航视图
2222
modelsHash: "", // 模型配置的哈希(用于脏检测)
2323
modelProvidersDraft: {}, // 模型提供商编辑草稿
24+
agentsDefaultsModelsDraft: {}, // Agent 默认模型 allowlist 草稿
25+
agentsDefaultModelDraft: null, // Agent 默认主模型草稿
2426
deletedModelProviderKeys: new Set(), // 待作为 tombstone 下发的 Provider 删除集合
27+
deletedAllowlistModelRefs: new Set(), // 待作为 tombstone 下发的 allowlist 模型引用集合
2528
modelsApply: { ... }, // 模型配置保存/热重启恢复状态
2629
providerModalOpen: false, // Provider 编辑弹窗是否打开
2730
skillModalOpen: false, // Skill 配置弹窗是否打开
@@ -73,6 +76,9 @@ let _skillsCache = []; // 技能数据缓存,供配置弹窗读取
7376
### 6. 模型配置热重启子状态
7477

7578
- `deletedModelProviderKeys`:记录用户删除或重命名后需要在 `config.patch` 中发送 `null` tombstone 的 provider key。
79+
- `agentsDefaultsModelsDraft`:当前模型页里“Agent 可用” allowlist 的前端草稿。
80+
- `agentsDefaultModelDraft`:当前模型页里“设为默认”对应的默认模型草稿。
81+
- `deletedAllowlistModelRefs`:记录取消勾选或删除后需要在 `config.patch` 中发送 `null` tombstone 的模型引用。
7682
- `modelsApply.phase``"idle" | "restarting" | "error"`,驱动“网关热重启中”状态徽标和按钮禁用 / 重试连接状态。
7783
- `modelsApply.message`:当前热重启提示文案;在轮询恢复期间动态更新。
7884

.trellis/spec/guides/code-reuse-thinking-guide.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,29 @@ When you've made similar changes to multiple files:
9797

9898
---
9999

100+
## Gotcha: Form State Exists in More Than One Helper Path
101+
102+
**Problem**: A UI feature adds a new state dimension (for example `allowlisted`, `isDefault`, selected filters, derived toggles), but only updates the obvious render/save path. Older helper paths such as:
103+
104+
- `render*()`
105+
- `collect*()`
106+
- `sync*FromForm()`
107+
- `fill*FromJson()`
108+
109+
still operate on the old state shape.
110+
111+
**Symptom**:
112+
- the UI looks correct while editing
113+
- clicking save silently drops the new selection
114+
- reopening the form shows the old value
115+
116+
**Prevention checklist**:
117+
- [ ] When adding any new form state, search for *all* render / collect / sync / fill helpers for that surface
118+
- [ ] If the form supports both raw JSON and structured controls, make JSON backfill explicit instead of automatic in submit helpers
119+
- [ ] Search for every call site of the normalization helper, not only the helper definition itself
120+
121+
---
122+
100123
## Checklist Before Commit
101124

102125
- [ ] Searched for existing similar code

.trellis/spec/guides/cross-layer-thinking-guide.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,21 @@ For each boundary:
6868

6969
**Good**: Each layer only knows its neighbors
7070

71+
### Mistake 4: Dual-Source Form State
72+
73+
**Bad**: A form has both:
74+
- visual controls (checkbox / radio / derived UI state)
75+
- raw JSON / text config
76+
77+
but the submit path silently replays the JSON -> form mapping right before save.
78+
79+
This overwrites the user's latest UI-only selections with stale serialized state.
80+
81+
**Good**:
82+
- define which layer is the source of truth at submit time
83+
- if JSON and form both exist, only run JSON -> form backfill on an explicit user action
84+
- never do implicit backfill inside `collect*()` / submit helpers unless every state dimension is serialized
85+
7186
---
7287

7388
## Checklist for Cross-Layer Features
@@ -77,11 +92,13 @@ Before implementation:
7792
- [ ] Identified all layer boundaries
7893
- [ ] Defined format at each boundary
7994
- [ ] Decided where validation happens
95+
- [ ] If the UI has both structured form fields and raw JSON, defined the single source of truth for submit
8096

8197
After implementation:
8298
- [ ] Tested with edge cases (null, empty, invalid)
8399
- [ ] Verified error handling at each boundary
84100
- [ ] Checked data survives round-trip
101+
- [ ] Checked that save-time normalization does not overwrite newer UI state with older serialized state
85102

86103
---
87104

.trellis/spec/guides/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ These guides help you **ask the right questions before coding**.
3434
- [ ] Data format changes between layers
3535
- [ ] Multiple consumers need the same data
3636
- [ ] You're not sure where to put some logic
37+
- [ ] A screen supports both structured form fields and raw JSON / text editing
3738

3839
→ Read [Cross-Layer Thinking Guide](./cross-layer-thinking-guide.md)
3940

@@ -42,6 +43,7 @@ These guides help you **ask the right questions before coding**.
4243
- [ ] You're writing similar code to something that exists
4344
- [ ] You see the same pattern repeated 3+ times
4445
- [ ] You're adding a new field to multiple places
46+
- [ ] You're adding a new UI state dimension to an existing form
4547
- [ ] **You're modifying any constant or config**
4648
- [ ] **You're creating a new utility/helper function** ← Search first!
4749

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
## Bug Analysis: 模型白名单勾选在保存前被静默回填冲掉
2+
3+
### 1. Root Cause Category
4+
- **Category**: C - Change Propagation Failure
5+
- **Specific Cause**: 模型管理页新增了每个模型行的策略状态(`allowlisted` / `isDefault`),但旧的 JSON 双向编辑辅助链路没有一起更新。`collectCurrentProvider()` 在提交前仍会调用 `fillProviderFormFromJson()`,它会按旧的 JSON / draft 状态重建模型行,导致用户刚在 UI 里勾选的“允许 Agent 使用”在真正组装 payload 前就被覆盖掉。
6+
7+
### 2. Why Fixes Failed (if applicable)
8+
1. **第一次修复**:把 `ECONNRESET` 当成主要问题,补了“保存后自动核验配置是否生效”。
9+
原因:这是症状层修复,解决了“结果未知”的提示问题,但没有解决“发送前状态已经丢失”的根因。
10+
2. **第二次修复**:尝试在 `fillProviderFormFromJson()` 中保留当前行的策略选择。
11+
原因:方向接近,但浏览器仍可能吃旧缓存,而且只修 backfill helper 不如直接去掉提交前的隐式 backfill 更可靠。
12+
3. **最终修复**:移除 `collectCurrentProvider()` 中的静默 `fillProviderFormFromJson()`,让 JSON -> 表单回填只发生在用户显式点击“回填表单”时。
13+
结果:`GLM-5` 在 UI 中勾选“允许 Agent 使用”后,保存当前供应商、保存并应用、重新打开弹窗时都能保持勾选。
14+
15+
### 3. Prevention Mechanisms
16+
| Priority | Mechanism | Specific Action | Status |
17+
|----------|-----------|-----------------|--------|
18+
| P0 | Documentation |`cross-layer-thinking-guide.md` 增加“双来源表单状态”陷阱,明确禁止在提交前做隐式 JSON -> 表单回填 | DONE |
19+
| P0 | Documentation |`code-reuse-thinking-guide.md` 增加“render / collect / sync / fill helper 必须一起搜”的 gotcha | DONE |
20+
| P1 | Documentation |`guides/index.md` 增加新触发器:表单既支持结构化字段又支持 JSON 编辑时,必须做 cross-layer 思考 | DONE |
21+
| P1 | Architecture | 让提交路径只读取当前表单状态;JSON 回填只能由显式按钮触发 | DONE |
22+
| P1 | Runtime |`config.patch` 过程中出现的 `ECONNRESET` / 热重启断链做保存后核验 | DONE |
23+
| P2 | Test Coverage | 增加一条浏览器级回归测试:勾选白名单 -> 保存当前供应商 -> 保存并应用 -> 重新打开仍保持勾选 | TODO |
24+
25+
### 4. Systematic Expansion
26+
- **Similar Issues**:
27+
- 模型页现有的 `syncProviderJsonFromForm()` / `fillProviderFormFromJson()` / `collectCurrentProvider()` 这组 helper 以后再加字段时仍有同类风险
28+
- 任何“表单 + 原始 JSON”双编辑面都可能复发,尤其是 Cron / 未来的节点或 Agent 配置页
29+
- **Design Improvement**:
30+
- 对双编辑面的提交规则做统一约束:提交只读当前表单状态;原始 JSON 只能通过显式“应用 / 回填”参与
31+
- 新的策略字段不应只存在于 DOM,最好尽量进入可比较/可验证的 draft state
32+
- **Process Improvement**:
33+
- 新增表单字段时,把 `render* / collect* / sync* / fill*` 当成一个固定搜索清单
34+
- 遇到“保存成功提示异常”时,先验证 payload 是否真包含新状态,再考虑网络 / 重启层
35+
36+
### 5. Knowledge Capture
37+
- [x] 更新 `.trellis/spec/guides/cross-layer-thinking-guide.md`
38+
- [x] 更新 `.trellis/spec/guides/code-reuse-thinking-guide.md`
39+
- [x] 更新 `.trellis/spec/guides/index.md`
40+
- [x] 在任务目录记录本次 bug 分析
41+
- [ ] `src/templates/markdown/spec/` 不存在,当前无法执行模板同步;若后续引入模板目录,应补上同步流程

0 commit comments

Comments
 (0)