877 Commits

Author SHA1 Message Date
xfy
82315853a2 fix(tiptap): slash 命令创建可运行代码块时模态框被立即关闭
斜杠菜单中按 Enter 选择「可运行代码块」时,openRunnableModal 在
同一个 keydown 事件处理中同步调用。模态框的 document keydown 监听
立即注册后,该 Enter 事件继续冒泡到 document 触发模态框的 Enter
处理——insert() → close(),模态框在同一 tick 内创建又销毁,用户
看不到配置弹框。

将 document.addEventListener("keydown", ...) 延迟到 setTimeout(0)
注册,确保触发事件完成传播后才开始监听。
2026-07-07 17:51:06 +08:00
xfy
1fa1984703 feat(editor): 点语言标签编辑可运行代码块的语言与配置
openRunnableModal 新增编辑模式(editPos + currentInfo):
- 回填当前块的 lang + overrides(解析 info string)
- 标题改「编辑」、按钮改「保存」
- 确认用 setNodeMarkup 原地更新 language 属性(保留块内代码)

CodeBlockNodeView 的语言标签在 runnable 块上可点击(绿字 hover 提示),
点击触发编辑模式(getPos 传 NodeView)。update() 在 language 变化时同步
刷新 <code> 的 class,让高亮按新语言重算。

补 4 个编辑模式用例(回填/标题/保存走 setNodeMarkup/改语言写入)。
Playwright 验证:python→node 编辑后 lang 标签与 code class 同步更新。
2026-07-07 16:12:11 +08:00
xfy
9c8ab791d9 fix(editor): codeBlock 内 Backspace 在 lowlight decoration 重建后失效
根因:CodeBlockLowlight 的 lowlight 插件在 codeBlock 内容变化后重新高亮,
重建 <code> 内 text node(包成 <span class=hljs-...>)。重建过程导致 DOM
selection 错位到 <pre>(contentDOM 父元素)而非 <code> 内文本,后续 Backspace
删不掉剩余字符,块删空后也无法删除整个 codeBlock。

文档层 ProseMirror selection($anchor)始终正确,只是 DOM 层不同步。用独立
Extension + handleKeyDown plugin prop 接管 codeBlock 内的 Backspace:基于
文档 pos 直接删除字符(不依赖 DOM selection),或在块空时 setNodeMarkup
转段落。handleKeyDown 优先级高于 keymap,能覆盖父类 CodeBlock 的同名 shortcut
(在 CodeBlockLowlight.extend 里 addKeyboardShortcuts 无法覆盖父类,故独立扩展)。

经 Playwright 真实浏览器逐键验证:输入 ab → 删 b → 删 a(此前失败) → 删块,
全链路正常。与 NodeView 无关(禁用 NodeView 同样复现,根因在 lowlight decoration)。
2026-07-07 14:05:53 +08:00
xfy
2a3a48af4e docs(agents): 记录 Tiptap 交互 bug 用 Playwright 调试的方法论
happy-dom 测不出 ProseMirror 真实交互 bug(键盘/selection/NodeView/
Suggestion range)。记录 Playwright 反馈环的标准搭建与诊断模式:
逐字符输入、调试写 DOM、二分定位 NodeView。附 /code 文本残留案例。

这类 bug 不进 git:repro.mjs/package.json/node_modules 用完即删。
2026-07-07 12:01:55 +08:00
xfy
df8f7397b2 fix(editor): 斜杠命令文本残留进新节点(/code 带入 codeBlock)
Suggestion 传给 command 的 range 在某些输入路径下过时——停在首次
输入 '/' 的位置 {from:1,to:2},不随后续字符更新。deleteRange 只删 '/',
命令文本(如 'code')残留,被 toggleCodeBlock 包进新节点。

该 bug 同时导致衍生症状:残留文本让 codeBlock 非空,Backspace 的
clearNodes 条件(!textContent.length)不成立,空块退格看似无效。

修复:command 转发处防御性重算 range——基于当前 selection 在同一段落
内往前找触发字符 '/',删到光标位置。对所有命令统一生效。

经 Playwright 真实浏览器复现验证(happy-dom 无法复现此 bug):
- /code 回车:codeBlock 现为空(原残留 'code' 文本)
- 空 codeBlock 退格:正常删除变段落(原无反应)
2026-07-07 11:59:26 +08:00
xfy
7fbaae7622 feat(editor): 斜杠命令空状态显示「无匹配命令」
搜索无结果时浮层显示居中提示(非空白卡片)。同步修两个空列表 bug:
- ArrowUp/Down 对 % 0 产生 NaN(除零)
- Enter 被吞(preventDefault 后无项可选,用户回车无反应)

空列表时方向键/回车放行(return false),仅 Escape 拦截关闭。
导出 createPopup 补 6 个 happy-dom 用例。
2026-07-07 10:24:54 +08:00
xfy
9a90514470 feat(editor): 斜杠命令支持英文别名搜索(中英文互通)
每条命令补 keywords 字段(空格分隔英文/常见词),搜索时 title/description/
keywords 任一命中即匹配。/code 现在能命中「代码块」,/link 命中「链接」。

提取 matchCommand 纯函数(原 inline filter),补 30 个用例覆盖中英文命中、
大小写不敏感、keywords 缺省回退、不命中场景。
2026-07-07 10:15:44 +08:00
xfy
d8036c6e70 fix(editor): 代码块内 Backspace 删整块(ignoreMutation 误忽略 contentDOM 编辑)
CodeBlockNodeView.ignoreMutation 无条件返回 true,导致 contentDOM(<code>)
内的 characterData/childList mutation 被忽略,ProseMirror 文档状态与 DOM
失同步——输入文字它不知道,Backspace 时基于错误状态重建,表现为删整块。

改为对齐 ProseMirror/Tiptap 默认 NodeView 逻辑:
- contentDOM 内的 mutation(characterData/childList)→ false(交给 PM)
- selection → false
- contentDOM 自身 attributes(高亮改 class)→ true(忽略)
- 工具栏/结果区装饰元素 → true(忽略)

补 2 个回归用例覆盖 contentDOM 内/外分支。参数类型改 ViewMutationRecord
(含 selection,ProseMirror 扩展类型)。
2026-07-07 10:03:36 +08:00
xfy
c8b2248066 fix(editor): runnable 块 classList.add 抛 InvalidCharacterError
CodeBlockNodeView 构造时把完整 info string 原样拼进 class
(`language-python runnable {...}`),classList.add 拒绝含空格 token,
导致编辑模式回填含 runnable 块的文章时崩溃(setMarkdown → addNode)。

改用 extractLang 只挂纯语言名 class。高亮靠 CodeBlockLowlight decoration,
本就不依赖此 class。补回归测试。
2026-07-07 09:41:05 +08:00
xfy
05460f1257 Merge branch 'feat/editor-runnable-codeblock'
编辑器三大增强:
- 斜杠命令插入可运行代码块(模态框配置语言+overrides)
- CodeBlockLowlight 语法高亮(Catppuccin Latte/Mocha,对齐读者侧)
- CodeBlockNodeView:语言标签 + 运行按钮(仅 runnable) + 内联运行结果

运行经 onRunCode 回调桥接到 Rust(start_exec + 轮询 get_exec_result)。
markdown 往返保真(language 承载完整 info string)。
2026-07-06 18:34:54 +08:00
xfy
763edc300d feat(editor): NodeView 适配 onRunCode 协议(extractLang + overridesJson 提取)
runCode 改传纯语言名(extractLang) + overrides JSON 字符串(extractOverridesJson),
对齐 Task 4 Rust 桥接 RunCodeOptsJs(language/source/overrides_json):
- highlight.ts: 新增 extractOverridesJson,对齐后端 parse_fence_info 取以 { 开头的 token
- code-block-view.ts: RunCodeOpts 加 overridesJson 字段,runCode 改传 extractLang/extractOverridesJson 提取结果
- index.ts: EditorOptions.onRunCode 类型同步加 overridesJson
- 测试: extractOverridesJson 6 例 + code-block-view 点击断言更新(纯语言名 + overridesJson)
2026-07-06 18:17:33 +08:00
xfy
0c1e8b7640 feat(bridge): 新增 make_run_code_closure 桥接编辑器内运行代码
tiptap_bridge.rs:
- EditorOptions extern 追加 set_on_run_code setter (js_name=onRunCode)
- 新增 RunCodeOptsJs 映射(language/source/overridesJson getter)
- 新增 format_run_result + make_run_code_closure
  · start_exec + 500ms 轮询 get_exec_result,30s 上限
  · overridesJson 空串视为 None,畸形 JSON 静默降级
  · 不依赖 server-only 的 languages 模块(前端 extractLang 已提取纯语言名)
- EditorHandle 加 _on_run_code 字段 + new 参数
- pub use 导出 make_run_code_closure + RunCodeOptsJs

write.rs: 跟进 EditorHandle::new 新签名(构造 on_run_code + set_on_run_code)。
2026-07-06 18:11:28 +08:00
xfy
6bdc48e84c feat(editor): CodeBlock NodeView 工具栏与结果区样式
语言标签 + 运行按钮 + 结果区,亮/暗双套 Catppuccin 配色。
runnable 绿条移到 .tiptap-codeblock 容器(替换旧 pre 绿条规则)。
2026-07-06 18:02:32 +08:00
xfy
6e5ff72a29 feat(editor): 挂载 CodeBlockNodeView + editor.storage 注入 onRunCode
CodeBlockLowlight.extend({ addNodeView }) 接入自定义 NodeView。
EditorOptions 新增 onRunCode 字段;编辑器初始化时挂到 editor.storage,
NodeView 经 storage key 读取(仿 upload-coordinator 范式)。
2026-07-06 18:00:15 +08:00
xfy
82280e3d4f feat(editor): CodeBlockNodeView 显示语言标签与运行按钮
自定义 NodeView 包裹 <pre><code>,顶部工具栏显示语言(extractLang 提取),
runnable 块额外显示运行按钮。contentDOM 指向 <code>,保证 CodeBlockLowlight
decoration(高亮)正常生效。运行走 editor.storage.__onRunCode 回调。
2026-07-06 17:59:01 +08:00
xfy
0adc6d8382 fix(editor): 暗色 type 色值修正 + extractLang 小写化
- .dark .hljs-type/.hljs-title.class_ 由 #8839ef(紫) 改为 #f9e2af(黄),
  与 spec 表、官方 @catppuccin/highlightjs、读者侧 highlight.css 三处对齐
- extractLang 加 toLowerCase,与后端 parse_fence_info(languages.rs:85)对齐,
  避免大写语言名触发 lowlight Unknown language 抛错
2026-07-06 16:13:08 +08:00
xfy
4de48cd16c style(editor): biome 修正 import 排序(highlight/index)
TDD 阶段子代理未跑 biome check --write,补齐 import 字母序以过 make lint。
2026-07-06 15:53:16 +08:00
xfy
8493e4c4c4 feat(editor): 代码块语法高亮配色(Catppuccin Latte/Mocha)
色值取自 @catppuccin/highlightjs@1.0.1,与读者侧 highlight.css 同一 palette。
亮/暗双套,.dark 前缀切换,scope 到 .tiptap-editor 避免污染全站。
2026-07-06 15:50:35 +08:00
xfy
e86fe0767d feat(editor): 用 CodeBlockLowlight 替换 StarterKit 自带 CodeBlock
禁用 StarterKit codeBlock,引入 CodeBlockLowlight + lowlight 实例。
代码块现在有实时语法高亮(ProseMirror decoration),language 属性
仍承载完整 info string(markdown 往返不变)。
2026-07-06 15:48:02 +08:00
xfy
e79e0d9ee2 feat(editor): 新增 lowlight 实例与 extractLang 语言提取
common 集(~35 种语言)注册到 lowlight;extractLang 从完整 info string
(如 'python runnable {...}')提取首 token,使 runnable 块按正确语言高亮。
2026-07-06 15:45:42 +08:00
xfy
0b4346ecd4 fix(editor): 语言下拉展开时 Enter 不误触发插入
<select> 展开时 Enter 用于确认选项,不应提交整个弹框。
HTMLSelectElement.open 浏览器存在但 TS lib.dom 未声明,用交叉类型断言。
2026-07-06 14:15:25 +08:00
xfy
9b4c0ea36e style(editor): biome 格式化 runnable 模态框代码
biome check --write 修正 import 排序 + 格式化 openRunnableModal 链式调用
(TDD 阶段子代理未跑 make fix,此处补齐以过 make lint)。
2026-07-06 14:08:06 +08:00
xfy
01ceef2694 feat(editor): 可运行代码块模态框样式 + runnable 块绿色边框
模态框复用 Catppuccin Latte/Mocha 配色;编辑器内含 runnable 标记的
代码块加左侧绿色边框(与 Code Runner 运行按钮主色一致)。
2026-07-06 14:04:52 +08:00
xfy
d6fc98d253 feat(editor): 斜杠菜单新增「可运行代码块」条目
触发 openRunnableModal,配置后插入标准 CodeBlock(language 承载
'python runnable {...}' info string)。
2026-07-06 14:02:12 +08:00
xfy
5929ab68b4 feat(editor): 新增 openRunnableModal 模态框配置可运行代码块
原生 DOM 模态框(遮罩+卡片),语言 select + 超时/内存/网络 3 项 overrides
+ 实时预览。确认插入 setCodeBlock({language: 'python runnable {...}'}),
Esc/遮罩/取消关闭不插入。
2026-07-06 13:58:51 +08:00
xfy
84b3da9db6 feat(editor): 新增 buildRunnableInfo 构造 runnable fence info string
纯函数,把语言+overrides 配置转成 'python runnable {...}' 形态。
dirty=false 省略 JSON,dirty=true 写 timeout/memory/network 三项,
字段顺序固定。
2026-07-06 13:45:11 +08:00
xfy
89bab26b90 fix(lint): themes.ts import 排序(type import 前置)
Biome organizeImports 要求 import type 排在普通 import 之前,
交换 @codemirror/view 与 @codemirror/state 顺序。make lint 全绿。
2026-07-06 11:45:14 +08:00
xfy
9231e993e4 fix(clippy): 修复两处预存 clippy 错误
- execute.rs: ExecResult/ExecStatus 仅 server 端使用,加 cfg gate
  消除 WASM 目标的 unused_imports
- system.rs: in_scope 外层闭包保留 || execute_for_editor() 包装
  (Closure::new 要求 Fn 可重复调用,直接传闭包会 use-after-move),
  加 #[allow(clippy::redundant_closure)] 并注释原因

cargo clippy 双目标 -- -D warnings 均通过。
2026-07-06 11:42:03 +08:00
xfy
1db641bca9 refactor(sql-console): SqlConsoleTab 改用 SqlResultTable 组件
删除调用方对 Vec<Vec<Value>> → Vec<Vec<String>> 的预处理(保留类型
信息),结果表格段替换为 <SqlResultTable>,空结果集分支保留。
2026-07-06 11:32:33 +08:00
xfy
59edc7c4e4 feat(sql-console): 表头 sticky + 数据行截断 + 点击展开详情 2026-07-06 11:01:11 +08:00
xfy
b0833eebf7 feat(sql-console): 单元格类型化渲染 (NULL/布尔/数字/文本) 2026-07-06 10:33:46 +08:00
xfy
6d0dd8bb6e feat(sql-console): SqlResultTable 组件骨架 + 模块挂载 2026-07-06 10:30:30 +08:00
xfy
d3c2e41fd8 fix(codemirror): 编辑器塌缩导致上下背景割裂(height:100% 失效)
上一提交(3c10f72)用 `& { height: 100% }` 让 .cm-editor 撑满容器,
但实测无效——headless chromium 抓取 computed style 显示:

  容器#ed:      h=280px  (min-height)
  .cm-editor:   h=63px   ← 塌缩!只有内容高度
  .cm-gutters:  h=63px
  .cm-content:  h=63px

根因:CSS 百分比高度要求父元素有「明确 height」(不是 min-height)。
容器只有 min-height: 280px,没有 height,`.cm-editor { height: 100% }`
解析为 auto,塌缩到内容高度(3 行 ≈ 63px)。剩余 217px 透出容器背景
(paper-entry/mantle),与 .cm-editor 的 base 色形成上下割裂。

修复:改用 flexbox 而非百分比高度——
- themes.ts:`& { flex: 1 1 0; minHeight: 0 }`(替换 height:100%)
- 容器(system.rs / runner.rs):`display: flex; flex-direction: column`

flex 子项的 `flex: 1` 不依赖父元素 height,能真正填满 min-height 撑开
的空间。headless 验证:.cm-editor 现为 280px,与容器一致,割裂消失。

诊断方法:构建自包含测试 HTML(内联 editor.js),chromium --headless
--dump-dom 抓 getComputedStyle。这是应有的反馈循环,避免了前几轮
凭代码猜测导致的反复误诊。
2026-07-06 00:20:55 +08:00
xfy
b207babdb6 refactor(admin): 统一按钮令牌,消除样式散落
将 7 个 admin 页面中复制粘贴/各自为政的按钮 class 收敛到 ui.rs 令牌:

主操作色系统一为主题绿(bg-paper-accent):
- dashboard/posts「发布文章」、write「发布/更新」+「确认」、runner 语言切换选中态
  改用 BTN_PRIMARY / BTN_PRIMARY_SM(原为深色 bg-paper-primary,现统一为绿)
- system 4 处刷新/执行/创建备份、trash 保存设置、write 发布按钮改用 LoadingButton
  统一 loading 态为 spinner 绝对居中叠加(按钮宽度不变,原 system 用文字切换、
  write 用整体变灰,现全部一致)

描边/图标/行内按钮统一:
- posts 重建、system 刷新列表 → BTN_OUTLINE
- trash 清空回收站 → BTN_DANGER_OUTLINE
- trash 步进 ± 按钮(2 处重复字符串)→ BTN_ICON
- write 关闭 × 按钮(2 处重复)→ BTN_CLOSE_ICON
- system BackupRow 恢复/删除绕过令牌 → BTN_TEXT_AMBER / BTN_TEXT_RED
  (删除色从 red-600 校正为令牌的 red-500,与其他行内删除一致)

消除约 15 处内联 class 字符串重复,system.rs 4 种主操作变体收敛为 1 种。
2026-07-06 00:17:25 +08:00
xfy
545651d304 refactor(ui): 新增 BTN_PRIMARY/BTN_OUTLINE 等按钮令牌与 LoadingButton 组件
消除 admin 页面按钮样式散落问题:
- BTN_PRIMARY / BTN_PRIMARY_SM:主题绿实心胶囊主操作按钮
- BTN_OUTLINE:描边次要按钮(relative 以承载 spinner 叠加)
- BTN_DANGER_OUTLINE:红色描边危险按钮(清空回收站)
- BTN_CLOSE_ICON / BTN_ICON:× 关闭与方形步进图标按钮
- LoadingButton 组件:封装主题绿三态(loading spinner 叠加 / disabled 灰化 / 正常),
  统一全站 loading 态为 posts.rs 的 spinner 叠加模式(按钮宽度不变,避免布局抖动)
2026-07-06 00:17:09 +08:00
xfy
3c10f72444 fix(codemirror): 编辑器未撑满容器导致上下背景割裂
现象:编辑器「有行号的上半部分」与「空白下半部分」背景色不一致。

根因:CodeMirror core 的 base theme 给 `.cm-editor`(&)只设了
position: relative + display: flex,没有 height。编辑器默认只占
内容高度(几行 ≈ 60px),不填满父容器。容器 div 设了 min-height:
280px,但 .cm-editor 不会自动撑到 280px——剩余空间透出容器的
paper-entry(mantle #e6e9ef) 背景,与 .cm-editor 的 base(#eff1f5)
形成「上半 base / 下半 mantle」的上下色差。

修复:themes.ts 的覆盖里加 `& { height: 100% }`,让 .cm-editor
撑满父容器。配合容器已有的 min-height: 280px,编辑器完整填充,
背景统一为 base 色,上下割裂消失。
2026-07-06 00:06:00 +08:00
xfy
64907401a0 fix(codemirror): 行号区背景与代码区割裂(core base theme 覆盖 catppuccin)
现象:行号列(gutter)背景是 #f5f5f5 浅灰,代码区(content)是 catppuccin
base #eff1f5 浅蓝灰,两者不一致,有明显割裂。

误诊修正:上一提交(e1b312e)以为是容器背景与 CodeMirror 层次扁平,
改了容器为 code-block(crust)。但 CodeMirror 内部 .cm-editor 和
.cm-gutters 都有不透明背景,容器色根本透不出来——所以「没效果」。
本次回退该容器改动。

真因:CodeMirror core 的内置 base theme(@codemirror/view dist index.js:6916)
给 `&light .cm-gutters` 设了 backgroundColor: "#f5f5f5" / dark "#333338",
特异性为 .cm-editor.cm-light .cm-gutters,高于 @catppuccin/codemirror
的 .cm-gutters 覆盖,catppuccin 的 base 背景被 core 默认灰压制。

修复:themes.ts 在 catppuccin extension 之后追加一个 EditorView.theme,
用 backgroundColor: transparent !important 强制 .cm-gutters 透明,
继承 .cm-editor 的 base 色,行号区与代码区完全融合。light/dark 同理。
2026-07-05 23:58:18 +08:00
xfy
e1b312e651 fix(admin): SQL 编辑器 gutter 与 content 背景割裂
现象:行号区和代码输入区背景色看起来不一致,有割裂感。

根因:三层背景色各不相同——容器用 paper-entry(mantle #e6e9ef)、
标题栏用 paper-theme(base #eff1f5)、CodeMirror 自带 base 背景。
容器与编辑器同处 base/mantle 之间,层次扁平,CodeMirror 的 base
浮不出层次,gutter 与 content 的细微差异被放大。

修复:建立 crust > mantle > base 三层明确递进——
- 容器改 paper-code-block(crust #dce0e8,最深)
- 标题栏改 paper-entry(mantle,中)
- CodeMirror 保持自带 base(最浅)

编辑器作为「浅色岛」浮在深色容器上(VS Code / Sublime 的 IDE
经典范式),gutter 与 content 同为 base 完全融合,割裂消失。
2026-07-05 23:10:56 +08:00
xfy
a7a97fc299 fix(code-runner): 去掉 nproc ulimit,修复容器启动 EAGAIN
容器启动时报 'exec /bin/sh: resource temporarily unavailable',根因是
HostConfig 里的 ulimit nproc=64 与 non-root user(1000:1000) 组合:
RLIMIT_NPROC 按 UID 计数且在容器初始 exec 阶段就生效,与容器内实际进程数无关,
导致 sh 都起不来。pids_limit=64 已在 cgroup 层兜底进程数限制,nproc 是冗余且
有害的双重约束。

复现:docker run --ulimit nproc=64 --user 1000:1000 → exec 失败
       去掉 nproc(保留 nofile + pids_limit + 其他沙箱约束)→ python/node 正常运行

cargo test infra::docker 全过。
2026-07-05 22:05:01 +08:00
xfy
2cfc64b319 fix(admin): SQL 控制台 Ctrl+Enter 触发 panic(无 dioxus scope)
现象:编辑器有焦点时点击执行(或按 Ctrl+Enter)会 panic:
  called `Option::unwrap()` on a `None` value  (runtime.rs:223)
  RefCell already borrowed                       (runtime.rs:280)

根因:Ctrl+Enter 从 CodeMirror 的 JS 事件处理中触发 onRunShortcut
回调,此时 dioxus scope stack 为空。execute_for_editor 内部调
spawn(),而 spawn() 走 Runtime::with_current_scope →
current_scope_id().unwrap(),scope stack 空时直接 panic。
第二个 panic 是 unwind 期间再次 borrow scope_stack 触发的。

修复:用 dioxus 文档推荐的 web-sys 回调模式——在组件主体捕获
scope_id(此时 scope 活跃),JS 回调里用
Runtime::current().in_scope(scope_id, ...) 重建 scope 上下文
后再执行,spawn 即可找到 origin scope。

dioxus-core 源码(global_context.rs docstring)明示此模式,
dioxus-signals/src/global/mod.rs:201 也是这样用的。

注意:按钮 onclick 走 dioxus 事件系统(scope 已设),不受影响;
仅 CodeMirror JS 回调路径需要 in_scope 兜底。
2026-07-05 21:59:17 +08:00
xfy
81648de16f fix(docker): runner-base 切换 apk 源为清华镜像
国内访问 dl-cdn.alpinelinux.org 经常超时,导致 apk add python3/nodejs 失败
('temporary error' / 'no such package')。在基础镜像里用 sed 把 apk repositories
指向 mirrors.tuna.tsinghua.edu.cn,python/node 镜像 FROM base 自动继承,无需重复修改。
2026-07-05 21:57:44 +08:00
xfy
fe6934ddd5 feat(admin): SQL 控制台接通 Ctrl+Enter 并重做视觉
接通上一提交打通的快捷键通道,同时重做编辑器/工具条/结果区视觉。

功能(接通 Ctrl+Enter):
- 把 run_sql 逻辑拆出 execute_for_editor 闭包(仅捕获 Copy 的 Signal),
  注入 CodeMirror onRunShortcut,按钮 onclick 仍走 run_sql
- 两条路径共用同一套 4 道护栏 + 资源逻辑,行为完全等价

视觉(编辑器标题栏一体化):
- 编辑器加标题栏:左 accent 状态点 + SQL 标识,右 ⌘↵ 快捷键提示
- 容器升至 rounded-2xl + 280px 最小高度,留呼吸
- 执行按钮改胶囊式(rounded-full)+ active:scale 触觉反馈 +
  SPINNER_SVG 替代纯文字「执行中...」
- 危险选项(我了解后果)用竖线分隔 + 红色语义,与普通选项分层
- 结果摘要从裸文本改徽章式(accent-soft 底 + 圆角胶囊)
- 加空结果友好态:SELECT 返回空集时显示「查询成功,无返回行」
- EXPLAIN 输出 pre 补 m-0,消除默认外边距
2026-07-05 21:31:16 +08:00
xfy
4239602f3d feat(codemirror): 支持 Ctrl/Cmd+Enter 运行快捷键
按钮一直写着「执行 (Ctrl+Enter)」但快捷键从未接线——basicSetup 不含
自定义 keymap。从底层往上打通 Mod-Enter 运行通道:

editor.ts:
- EditorOptions 加 onRunShortcut 可选回调
- constructor 条件注入 keymap.of([{ key: "Mod-Enter", ... }])
- 用 Prec.highest 包裹:@replit/codemirror-vim 的 vimPlugin 在
  ViewPlugin 层拦截按键,普通 keymap 会被吞;vim 自己也用
  Prec.highest,跟随此惯例保证 Mod-Enter 在 vim 之前命中
- 放在 vimCompartment 之前,满足「vim 必须在 keymap 最前」约束
- Prec 从 @codemirror/state 引入(不在 @codemirror/view)

codemirror_bridge.rs:
- EditorOptions extern 加 set_on_run_shortcut
- EditorHandle 加 _on_run_shortcut 字段保生命周期,new() 签名增加
  该参数(Drop 随实例释放)

调用点适配:
- CodeRunner(runner.rs)传 no-op 闭包(不用该功能但签名要满足)
- SQL 控制台的接线在后续 system.rs 改动中完成
2026-07-05 21:31:03 +08:00
xfy
1dc02561a3 style(code-runner): 重做 admin/runner 视觉,对齐项目 token 体系
页面与 CodeRunner 组件此前用的是 daisyUI 类(base-100/btn-primary/
input-bordered/bg-neutral 等),但项目根本没装 daisyUI——这些类在
编译产物里 0 匹配,导致页面渲染成裸结构,是「半成品」观感的根因。

全部换成项目真实的 --color-paper-* 语义 token,视觉与 dashboard /
posts / system 等后台页面统一:

页面壳(runner.rs):
- h1 升至 text-4xl,与其它 admin 页一致;加底部分割线
- 外层对齐 max-w-5xl space-y-8
- 语言切换改为胶囊按钮组(选中态 accent 绿实心)
- overrides 输入框套 system.rs 的表单范式,加 focus 态
- 错误/提示用 red-500 dark: red-400,解析成功时显示字段说明

CodeRunner 组件(runner.rs):
- 容器改 rounded-2xl + paper-border + paper-entry
- 顶栏加状态点(accent)+ 胶囊运行按钮(active 缩放反馈)
- 运行中用 SPINNER_SVG 替代 daisyUI loading-spinner
- 输出区改 paper-code-block 底 + 语义色文字,随主题切
- 错误区改 red-50 dark: red-900/10 柔光块
2026-07-05 14:49:24 +08:00
xfy
c83b834c8b feat(code-runner): admin 跳过代码运行速率限制
作者在 /admin/runner 沙箱调试代码块时,1 次/秒 + 50 次/天的读者限流
(RATE_LIMIT_CODE_EXEC_*)会频繁打断试运行。已登录 admin 改为跳过
check_code_exec_limit,仍受并发槽、资源钳制与源码大小校验约束。
2026-07-05 14:32:56 +08:00
xfy
575b6b533e feat(code-runner): CodeRunner 挂载 CodeMirror 编辑器,支持双向绑定
兑现组件一直承诺但无人实现的「调用方挂载编辑器」契约——此前容器 div 只
输出 id,admin/runner 沙箱与文章正文 runnable 块都只能提交写死的初始源码,
作者/读者无法编辑。

- CodeRunner 在 WASM 端按自身 container_id 调 codemirror_bridge.create
  挂载编辑器,onChange 回写内部 source_signal(编辑器内容 = 唯一真源)
- run_code 改读 source_signal 当前值,提交编辑后内容而非初始快照
- 主题切换经 use_resolved_theme 订阅,热切换编辑器主题
- use_drop 销毁 EditorHandle,释放 CodeMirror 实例与 closure 槽位
- source prop 外部变更(admin 切换语言重置示例)同步进 signal + 编辑器
- admin/runner 页面 overrides 解析挪进 use_memo,避免 render 体写状态

范式镜像 SQL 控制台(system.rs)与 Tiptap 编辑器(write.rs)。
2026-07-05 14:32:39 +08:00
xfy
58f1a981d3 feat(admin): SSR 层认证守卫,未登录直接跳登录页
此前后台鉴权完全在客户端 WASM 完成:SSR 渲染骨架屏 → 浏览器下载
/编译 WASM → hydrate → 异步 get_current_user() → 客户端 navigator.
push(Login)。整条链串行,未登录用户首屏要空白好久才跳转登录。

新增 admin_guard 中间件:/admin* 请求在进入 Dioxus SSR 渲染器之前
读 session cookie → get_user_by_token 校验 admin 角色 → 失败直接 302
到 /login。未登录用户首屏即登录页,零 SSR 渲染开销。

- 置于 app_routes layer 链最外层(CSRF/cache/SSR 之前短路)
- 复用 get_user_by_token:命中内存缓存 + session_generation 校验,
  与客户端鉴权同一套语义
- DB 错误 fail-open(避免抖动把已登录管理员踢登录页),客户端
  AdminLayout 仍作兜底
- /admin 与 /login 本就不进 SSR 缓存,302 不会被缓存
2026-07-04 22:53:28 +08:00
xfy
16bd644783 fix(admin): 仪表盘空数据时显示空状态占位
空库下 get_post_stats / list_posts 正常返回,但 list_posts 返回
Some([]) 时走有数据分支、循环 0 次,渲染出一个空表格容器,视觉
上仪表盘近期文章区域一片空白,没有空状态引导。

复用项目既有的 empty_state::EmptyState 约定(与 posts.rs / trash.rs /
comments.rs 列表页一致):Some(posts) 且 posts.is_empty() 时渲染
暂无文章插画 + 写文章CTA。空状态放在 ADMIN_TABLE_CLASS 容器
之外,避免 overflow-hidden 裁掉插画的 py-20 内边距。
2026-07-04 22:49:25 +08:00
xfy
383e6f6b43 fix(db): expand source chain for all DB error logging
The previous fix only covered migration errors. The same truncation
affected two more places, both because tokio_postgres::Error's Display
prints only "db error" while the real message lives in source():

- api/error.rs: AppError::{db_conn,query,tx} logged with {e}, so every
  runtime DB failure showed as "Query failed: db error". The
  constructors' bound is tightened from impl Display+Debug to
  impl std::error::Error so the source chain can be walked; the
  user-facing message stays sanitized (existing tests still pass).
- db/pool.rs: ensure_database formatted tokio_postgres::Error directly
  into the String returned to main.rs' exit(1) path, collapsing to
  "failed to query pg_database: db error" etc.

Adds a shared crate::db::format_with_sources helper (walks source()
with de-dup of placeholder layers) and reuses it from MigrateError's
Display, the AppError constructors, and pool.rs.
2026-07-04 21:54:54 +08:00
xfy
e9f0b032b7 fix(db): expand migration error display to walk source chain
tokio_postgres::Error's Display prints only the placeholder "db error"
for DB-side failures; the actual message (e.g. column already exists,
SQLSTATE, constraint name) lives in source() as DbError. Without
traversing the chain, startup logs showed the useless
"migration 001 failed: db error", making failures impossible to
diagnose.

MigrateError::Display now appends each source layer with ": " and
exposes source() so error-reporting layers that walk the chain
themselves also benefit.
2026-07-04 21:47:24 +08:00