diff --git a/workers/admin-panel/.impeccable/critique/2026-09-05T12-06-17Z__workers-admin-panel-web-src-views-history-view-tsx.md b/workers/admin-panel/.impeccable/critique/2026-09-05T12-06-17Z__workers-admin-panel-web-src-views-history-view-tsx.md new file mode 100644 index 0000000..4272eb0 --- /dev/null +++ b/workers/admin-panel/.impeccable/critique/2026-09-05T12-06-17Z__workers-admin-panel-web-src-views-history-view-tsx.md @@ -0,0 +1,182 @@ +--- +target: 前端发布历史 diff 功能 +total_score: 15 +max_score: 40 +na_heuristics: +p0_count: 2 +p1_count: 3 +timestamp: 2026-09-05T12-06-17Z +slug: workers-admin-panel-web-src-views-history-view-tsx +--- +Method: dual-agent(A 设计评审 · B 检测器 + 浏览器取证,两个隔离子代理并行;A 先完成,检测结果后进综合) + +评审对象:jouska 管理面板「发布历史 + 版本对比(diff)」 +主文件:`web/src/views/history-view.tsx`、`web/src/components/revision-diff.tsx`、`web/src/components/rollback-dialog.tsx`(服务端判定在 `src/diff.ts`) +Mode: **Operate**(操作者要完成任务:看懂两版之间改了什么、必要时退回去) + +## Design Health Score + +| # | 启发式 | 分 | 关键问题 | +|---|---|---|---| +| 1 | 系统状态可见 | 1 | 点满两版后视口零变化:diff 标题在折线下 51px(6 条 revision)/6.8 屏之外(50 条),页面不滚、不播报 | +| 2 | 贴近真实世界 | 2 | 「原值/新值」是相对词,而哪一侧是原值由点击顺序决定;标题用双向 `↔` | +| 3 | 用户控制与自由 | 2 | 选错一张不能只换那一张;Esc 全页无作用;同一对 diff 失败后**无法重试**(effect 依赖 `[from,to]` 不变) | +| 4 | 一致性与标准 | 2 | 回滚弹窗与发布弹窗同源(优秀);但同页三套报错语法、三套「你不能做」答法,且与 `audit-view` 的四处成例都不一样 | +| 5 | 防错 | 1 | 方向可静默反转的 diff = 决策错误生成器;键盘按回滚会**静默执行另一个动作** | +| 6 | 识别优于回忆 | 1 | 读懂一行要同时记 7–8 件事,其中 3 件不在屏幕上 | +| 7 | 灵活与高效 | 1 | 零快捷键、无「与线上比」、无复制、无分页;50 条时主路径含 6.8 屏手动滚动 | +| 8 | 美观与极简 | 2 | 视觉权重反了:`新增` 是全系统最重的 primary 实底,`删除` 只有 10% 淡染;时间轴竖线 10% 白且被 `gap-3` 切断 | +| 9 | 帮助识别/诊断/恢复 | 1 | 「有一侧没有快照」不说哪一侧(服务端 `detail` 里说了,客户端扔了);无重试;未知码裸奔 | +| 10 | 帮助与文档 | 2 | 文案好的地方非常好,但 diff 内零解释;唯一那句解释挂在 `pointer-events:none` 的禁用按钮 `title` 上,鼠标永远碰不到 | +| **合计** | | **15/40** | **Poor —— 主体验需要大修** | + +十条全部适用(Operate 面板),未使用 n/a。 + +分数低不是因为装饰层差,恰恰相反:**四个最重的缺陷全在主任务路径上**,而样式纪律是干净的。 + +## Design Specificity 判定 + +**列表是长出来的,diff 卡是搬过来的。** + +时间轴那一侧带着 jouska 的性格,而且是别的产品抄不走的那种:`live` 圆点换主色、`回滚自 #43` 把 rollback 出处写在卡上、`无快照` + 断档说明行承认记录的边界。这三样只在「一次发布 = 一次 KV 写 + 一份快照 + 一条审计」这个具体机制下才有意义。 + +diff 卡则是任何 config 后台都能原样搬走的形态,判据不是「像 git diff」,而是**它对 jouska 的领域一无所知**: + +- 面板自己有 `src/danger.ts`(12+ 条危险字段带 level)和 `lib/types.ts` 的 `DANGER_REASONS`(每条配一句中文后果)。`routes..access.keys` 就在里面,配着「粘错了真 key 的主人从此被挡在门外」。发布弹窗遇到它会铺 `danger-surface` 并要求亲手勾一次;diff 里它是第 8 行灰字,和 `timeoutMs: 30000 → 10000` 一模一样。同一个字段、同一个产品、两种立场。 +- `移序 第 5 位 → 第 1 位` 拿的是 badge 里最轻的 `outline`,还被排到最后 —— 而「顺序即优先级、首个匹配胜出」是 `shadow.ts` 存在的全部理由。 +- 路由 id 从不解析成 host/path,虽然 `matchSummary()` 就在旁边、路由页天天在用。 + +现成材料没用上的地方:服务端 `diffRoutes` 本来就是按 `id` 归并的,前端把这个结构拍平成按动词分组;`src/api/revisions.ts` 的注释写着 diff 端点方向自由是 "exactly what the rollback dialog needs" —— 门留了,回滚弹窗没走。 + +**确定性扫描(Assessment B)** + +CLI `detect.mjs --json` 扫这三个文件:**0 条发现,退出码 0**。这个「干净」是被反证过的,不是扫描器没开火:`--no-config --no-design-system` 绕过立档豁免仍是 0;同一扫描器扫整个 `web/src` 出 4 条 `design-system-font-size`(`App.tsx` 的 10px、`ui/button.tsx` 与 `ui/toggle.tsx` 的 0.8rem);人造探针文件命中 4 条、退出码 2。 + +浏览器内注入检测器:8 组发现,逐条判定后**真实归属这个功能的是 0 条**: + +| 规则 | 数量 | 判定 | +|---|---|---| +| `nested-cards` | 7 | **误报,机制可证**:`isCardLikeDOM` 的 `hasBorder` 靠正则 `/\bborder\b/` 匹配类名,命中的是 `CardHeader` 里的 variant 选择器 `[.border-b]:pb-…`,不是画出来的边框。历史页没有一处真的把 Card 套进 Card | +| `flat-type-hierarchy` | 1 | **误报,DESIGN.md 明文立档**:「封顶 16px、层级靠字重与灰度」「无大标题规则」。扫描器要的 ≥1.25 级差与这条规则直接冲突,规则赢 | +| `layout-transition` | 1 | 真实但不归这三个文件:`transition-all` 来自官方 registry 基座件(button/badge/toggle/tabs/accordion/switch) | +| `text-overflow` | 1 | 真实但不归历史页:顶栏账号名 `max-w-16 truncate` | +| `text-occlusion` | 7 | 覆盖层自检伪影(被遮的是覆盖层自己的标签) | + +**结论值得单独说一句:检测器全绿,而这一屏最重的四个问题一个都不在它的射程内。** 键盘路径、结果送达、diff 方向、危险语义 —— 没有一条是能用正则扫出来的。 + +**覆盖层**:注入成功(`detect.js` 加载、DOM 出现 15 个 `.impeccable-*` 元素、`window.impeccableDetect` 就位),但它跑在无头浏览器里且已关闭 —— **没有留给你看的常驻覆盖层**,上表就是它报的全部内容。为 critique 起的 vite(:5301)与 live-server(:8400)都已停,`pgrep` 自查无残留,仓库 `git status` 干净。 + +## Overall Impression + +**账本记得很老实,读账的那一半没做完。** + +时间轴这一半是这个仓库里质量最高的东西之一:它承认自己不知道的事(无快照、断档、路由数未知),而且分三种说法分别解释,没有拿一个 `—` 糊过去。回滚弹窗更是把「回滚就是一次发布」这个后端事实落成了前端形态,一个字都没重新发明。 + +diff 这一半停在了「把服务端给的字段摆出来」。它正确、诚实、密度也合适 —— 但真正在做 diff 的是人不是工具:你得自己记住哪一版是原值,自己知道 `allowPrivateUpstream` 是危险方向,自己把散在 6 行里的 `api-gw` 拼回一条路由,自己滚 6.8 屏去找结果。 + +**最大的一个机会**:这一屏真正的主角是 diff,不是列表。把它翻过来 —— 进页面就默认对比「正在服务的那一版」与它前一版,diff 在首屏,列表退成换参照物的选择器 —— 凌晨三点值班的人零点击就拿到答案,「任选两版」变成高级路径。现在是 6500px 的列表当主角。 + +## What's Working + +1. **三种「记录不完整」各说各的话。** 无快照说「早于历史功能,只留下审计记录 —— 能看,不能对比或回滚」;断档说「改动已上线、面板记录没写成,历史无法补记」;路由数未知就说未知。同类产品的做法是藏起来或给个假按钮。这里选了只说实话,而且没把三件事混成一句。 +2. **回滚复用发布的两段式闸门。** 骨架、`confirming && confirmed` 的合取、`danger-surface` 底色、在途锁关闭全部同源,结尾 toast 还补一句「线上约 3 分钟内全面生效」。操作者在两处看到同一张脸,不需要学第二套规则 —— 峰终定律靠这个救回来了。 +3. **卡上那行说明的选材是对的。** `12 条路由 · 把 api-gw 的上游切到新集群…(事故 INC-2291 的临时缓解)` —— 规模 + 意图放同一行,正是事后追查要的两样东西。(呈现有问题,见 P2,但选的东西对。) + +## Priority Issues + +### [P0] 结果渲染在视口之外,而且没有任何东西把人送过去 +**实测**:6 条 revision 时 diff 标题 `top=1001px`,视口 950px,点击前后 `scrollY` 全程 **0**,`scroll-behavior: auto`,无 `scrollIntoView`;文档从 1064px 直接翻倍到 2036px,**跳版发生在屏外**。50 条 revision(正好是快照滚动保留上限,所以成熟部署永久处在这个状态)时标题在 **6497px = 6.8 屏之下**;390 宽下 **8505px = 10.1 屏之下**。点满两版后视口里唯一的变化是那张卡多一枚 `已选 #46` badge、外加「对比」按钮的文字消失了。 +**为什么要紧**:主任务的完成信号不可见。失败态同样在折线之下 51px —— 用户点完第二张卡,连「对比不了」都看不到。加载态是一行 spinner,到达态是 956px 内容,DESIGN.md「加载完不跳版」在这一屏被以最大幅度违反。 +**修法**(按代价升序):(a) diff 卡移到列表**之前**,选中两版后插在头卡下方 —— 一处 JSX 换位,顺带解决「看完差异要回头找卡」;(b) 保留位置但选满两版时 `scrollIntoView({block:'start'})` **并**给 diff 卡加 `aria-live="polite"`,让键盘与读屏也收到通知;(c) diff 卡 sticky 在顶栏之下,复用路由编辑器动作栏的 `--panel-header-height` 语法。首选 (a)+(b)。 +**建议命令**:`/impeccable layout` + +### [P0] 「回滚」按钮键盘按不动,而且静默变成了另一个动作 +**实测两处取证**:聚焦 #45 的「回滚」→ 按 Enter → **弹窗不开**,改成这张卡被选进对比(`aria-pressed=true`);按空格 → 又取消选中;鼠标点同一个按钮 → 弹窗「回滚到 revision 45」正常打开。事件取证:`capture@card key=Enter target=BUTTON defaultPrevented=false` → `bubble@window … defaultPrevented=true`。 +**根因**:`history-view.tsx:97-102` 的 `onKeyDown` 挂在 `Card` 上,内层原生 `