2d5c7f3d2a
style(main): 修复缩进格式与模块/导入排序
CI / check (push) Has been cancelled
CI / build (push) Has been cancelled
2026-07-15 13:56:43 +08:00
acfdfb03f5
fix(image): 为 ImageFmt 别名补上 #[cfg(feature = "server")] 门控
...
type ImageFmt = image::ImageFormat 未加 server feature 门控,
而 image crate 是 server-only 可选依赖。WASM 构建 (web feature)
解析到该别名时找不到 image crate,报 E0433。
detect_format (唯一使用方) 已有 #[cfg(feature = "server")],
测试也在 #[cfg(all(test, feature = "server"))] 下,别名只需对齐门控。
2026-07-15 13:30:38 +08:00
8efd8c8021
perf(image): cache_key 单次拼接 + detect_format 零分配后缀匹配
...
CI / check (push) Has been cancelled
CI / build (push) Has been cancelled
cache_key:旧实现 vec![path.to_string()] + 最多 6 个 format! + join,
最坏 9 次堆分配(path clone + 6 个参数 String + Vec 指针数组 + join String)。
改用 with_capacity + write! 直写,1 次分配。每次未命中缓存的图片请求都走。
detect_format:旧实现对整条路径 to_lowercase() 分配 String,只为查后缀。
改用 rsplit('.').next() 取后缀 + eq_ignore_ascii_case 零分配匹配。
每次图片请求调用 1-2 次。
注:rotate 在 resize 之前是刻意设计(让 w/h 作用于旋转后的最终方向),
不动以保持 URL 语义与缓存一致性。
2026-07-15 11:48:59 +08:00
b269fcf205
perf(posts): list/search 零 capacity Vec 改 collect 预分配 + helpers retain
...
list.rs 5 处、search.rs 1 处用 Vec::new()+循环 push,初始 capacity 为 0,
循环触发多次指数 realloc。rows.iter().map().collect() 用 slice 迭代器的
精确 size_hint 一次性预分配。首页/列表/搜索/标签分页路径。
helpers.rs row_to_post_list_item / row_to_post_full:tags 从
into_iter().filter().collect() 改 retain 原地过滤。unwrap_or_default()
已返回 owned Vec,旧写法重建第二个 Vec。每行文章记录省一次 Vec 分配。
顺带修 slugify 一个 dead store(prev_dash=false 后被 true 无条件覆盖)。
2026-07-15 11:46:56 +08:00
a41a1d3ae1
perf(markdown): 消除双解析 + format! 改 write! 直写
...
双解析:旧实现对同一份 md 调两次 Parser::new_ext(一遍收集标题、
一遍生成 HTML),等于两倍的 tokenize + 解析 CPU。现 collect 成
Vec<Event> 一次,两遍遍历复用(Event 内含 CowStr 借用 md 切片,
collect 后仍可重复借用)。解析 CPU 减半。
format! → write!:标题开始/结束、TOC li、runnable pre、language class
共 5 处在循环内用 format! 拼接 HTML 片段,每次先分配临时 String 再
push_str 复制进去、临时串立即丢弃。改用 write!(html, ...) 直写目标
String,零中间分配。N 个标题省约 3N 次临时分配。
顺带:html 与 toc_html 用 with_capacity 预分配(旧 String::new 多次 realloc)。
全部 68 个 markdown 测试通过。
2026-07-15 11:41:55 +08:00
d4f8b463d2
perf(slug): slugify 单遍状态机重写,分配 4→1
...
旧实现 4 次堆分配、3 遍扫描:
1. title.to_lowercase() 分配整串 String(中文/符号根本不受影响却全量拷)
2. split('-').filter().collect::<Vec<&str>>() 分配 Vec
3. parts.join('-') 再分配 String
4. slug.chars().take(100).collect() 截断又分配
新实现用 prev_dash 状态机在单遍内完成:拼音成词、ASCII 小写化、
分隔符合并去首尾、按 char 截断到 100。1 次预分配、1 遍扫描。
与同仓库 markdown.rs::slugify_heading 的单遍写法对齐。
全部 27 个 slug 测试通过(含拼音边界、连续 dash、首尾 dash、下划线保留)。
2026-07-15 11:38:54 +08:00
b34f44d3e7
perf(html): escape_html 链式 5 次 replace 改单遍扫描
...
旧实现链式 5 次 str::replace,每次全量扫描 + 分配新 String,
5 个特殊字符 = 5 次堆分配 + 5 遍全文扫描,中间 4 个 String 立即丢弃。
新实现:先按字节快速检测是否含特殊字符(纯文本直接 clone 返回),
否则单遍 match 扫描 + 单次 with_capacity 分配。
分配 5→1(-80%),扫描 5→1。该函数被 markdown 渲染管线密集调用
(每个标题文本、每个 runnable 代码块的 overrides/source 转义)。
2026-07-15 11:32:36 +08:00
604febde98
perf(upload): 消除 data.to_vec() 多次全文件深拷贝
...
data 是引用计数的 Bytes,clone 廉价(原子 +1)。
- validate 路径:data.to_vec() → data.clone(),省一次全文件拷贝
- 转码闭包:original_data = data.to_vec() → data.clone(),
仅在「保留原格式」回退分支才 to_vec() 出落盘所需 Vec<u8>
- 路径拼接:用 chrono DelayedFormat 内联,省 year/month/day 三个中间 String
5MB 图片上传:转码成功路径从 10MB 堆分配降到 0(仅 Bytes 引用计数);
回退路径从 10MB 降到 5MB。
2026-07-15 11:28:21 +08:00
0835811487
style(header): 调整 nav 高度 80px → 64px (h-16)
2026-07-15 11:03:27 +08:00
ffa0a72e51
feat(admin/posts): tab 栏加平滑滑动指示器动画
...
PostsTabs 此前用 border-b-2 边框瞬切表示选中态,缺少 tab 切换的滑动动画。
移植 FilterTabs(system/comments 页)的滑动指示器机制:相对定位容器 + 绝对
定位滑块(贴底边盖住外层 border-b),WASM 端测量目标 button 的
offsetLeft/offsetWidth 动态定位,transition-all duration-300 驱动滑块从一个
tab 平滑滑到另一个。
选中态从 border-b-2 边框改为文字颜色区分(text-paper-primary),滑块负责
视觉指示。回收站数量角标保留不变。
与 FilterTabs 视觉一致,但 PostsTabs 因需支持角标故未直接复用 FilterTabs
(后者 items 是纯文本元组)。本地自建 POSTS_TAB_GROUP_ID 计数器给每处实例
唯一 DOM id 前缀。
2026-07-15 10:47:31 +08:00
50e64cd34c
fix(code_runner): use_resolved_theme 从 use_effect 闭包提到 cfg 块顶层
...
dx check 报 4 处 'hook called in a closure: use_resolved_theme':
use_resolved_theme() 是 hook(内部 use_context),被错误地放在 use_effect 的
move || { ... } 闭包内调用。Dioxus 规则要求 hook 只能在组件渲染路径顶层调用。
修复:在两个 #[cfg(target_arch="wasm32")] 块顶层各调用一次 use_resolved_theme()
拿到 Memo<ResolvedTheme>,move 进下方 effect 闭包;闭包内改为调用 memo
(resolved_theme())读值。调用 memo 本身不是 hook(Memo 是 Copy + Fn),
读值的同时建立主题订阅——主题切换时 effect 仍会重跑,行为不变。
范式对齐 ThemeToggle(src/theme.rs:341)的正确用法。
影响 4 处:CodeMirror 挂载/主题同步 effect、xterm 挂载/主题同步 effect。
2026-07-15 10:29:01 +08:00
0b40105341
feat(skill): 新增 optimizing-rust-performance 性能优化 skill
...
TDD-for-skills 流程提炼:先跑 baseline(两个 Rust 子代理无 skill 时
一致地「提及但未应用」Cow/SmallVec,且不量化收益、不提 profiling),
再写最小 skill 堵住这些缺口,GREEN 测试确认子代理带上 skill 后实际
应用所有模式并量化收益。
核心内容:
- 固定优先级心智模型(算法→内存→布局→并发)
- 5 个触发→动作模式:swap_remove / retain / take / SmallVec / Cow
- 热点路径额外模式(切片取词素、借用切片匹配、ASCII 谓词)
- 量化收益表 + profiling 提醒规范
- 过度优化防护:新增依赖代价、Cow 返回类型的 API 传染、决策速查表
2026-07-15 10:24:20 +08:00
25299d266f
refactor(admin): 合并 /admin/posts 与 /admin/posts/trash 为单路由 + tab 切换
...
将「全部文章」与「回收站」从两个独立路由(及各自的分页变体 PostsPage /
PostsTrashPage)合并为单一 /admin/posts 路由,用顶部 tab 在二者间切换。
tab 状态与翻页均改为客户端 signal 驱动(不走路由、不深链),与 system.rs 的
tab 模式一致:admin 内部页,刷新回到「全部文章」第 1 页。
路由瘦身:删除 PostsTrash / PostsTrashPage / PostsPage 三个变体,Router 不再
需要「静态段 trash 优先于 :page」的排序技巧。
主要改动:
- posts.rs: Posts 改为 tab 容器(PostsTab 枚举 + active_tab signal + key 化
match 渲染),header 文案随 tab 切换;PostsPage 主体下沉为 AllPostsList,
翻页改 current_page signal + use_paginated 回调式 Pagination。
- posts_trash.rs: PostsTrashPage → PostsTrashPanel(无参,signal 翻页),
删除入口组件与 PostsTabs import(header/tab 由容器统一提供)。
- PostsTabs: 从路由驱动(use_route)改为 signal 驱动(on_change 回调)。
- 修复 tab 水平对齐 bug:两个 tab 统一 inline-flex items-center 同盒模型 +
外层容器加 items-center,根除原先「全部文章」(inline 文本) 与「回收站」
(inline-flex 带角标) 盒模型不一致导致的垂直错位。
- admin_layout.rs: is_posts_route 简化为只匹配 Route::Posts{}。
- RebuildCacheBar 改为完全自持 state(rebuilding + result),结果消息行随组件
内联渲染,与父组件零耦合。
2026-07-15 10:21:50 +08:00
51e06cf118
refactor(ui): Pagination 支持可选 on_prev/on_next 回调翻页
...
翻页要本地化(走客户端 signal 而非路由)时,传入 on_prev/on_next 即渲染
button onclick;不传则保持原路由式 Link,前台/其他分页页面零影响。
为合并 /admin/posts 与 /admin/posts/trash 为单路由 tab 做准备:两个 tab
的列表翻页将改为 signal 驱动。prev_route/next_route 改为带默认值
(Route::Home {} 占位),回调存在时不被读取。
2026-07-15 10:00:40 +08:00
e18d2bfb2c
fix(docker): 用 tmpfs mode=1777 替换 uid/gid 选项以兼容 Podman
2026-07-14 16:42:04 +08:00
af3cd4b525
chore: gitignore public/xterm 构建产物
...
新增 xterm feature 时漏了同步 .gitignore,导致 xterm-terminal 的
vite 构建产物(terminal.{js,css,js.map})被提交进仓库——与其余
4 个 lib(tiptap/codemirror/lightbox/yggdrasil-core)一致,这些
IIFE 产物本就该忽略,源码在 libs/xterm-terminal/src/。
git rm --cached 三个产物文件(磁盘保留),.gitignore 补 public/xterm。
2026-07-14 16:36:51 +08:00
9bc8b5c2ee
refactor(libs): 抽取 @yggdrasil/shared 消除跨 IIFE 库的类型/常量重复
...
新增 libs/shared 内部包,作为跨 IIFE 库的单一真相源:
- ThemeName 类型(原 codemirror/themes.ts + xterm/themes.ts 各一份)
- THEME_CHANGE_EVENT 常量(原 yggdrasil-core 导出 + codemirror/xterm
各硬编码同名 string literal,靠注释维系一致)
- prefersReducedMotion 函数(原 lightbox + yggdrasil-core 各一份)
codemirror-editor / xterm-terminal / lightbox / yggdrasil-core 四个库
改为 workspace 依赖 @yggdrasil/shared,Vite 构建时 inline 进各自 IIFE,
不破坏 IIFE 隔离。消除「改一处忘改另一处导致静默失效」的风险。
附:image_reader_limits 已在上一提交共享;本提交是 libs 侧的对称改动。
2026-07-14 15:39:41 +08:00
6837b4cfa2
refactor(image): 合并维度读取函数,共享 image_reader_limits
...
提取 read_webp_dimensions / read_image_dimensions 两个核心函数,
read_dimensions_by_mime 与 read_dimensions_from_bytes 各自只负责
格式映射(MIME→format / 扩展名→format),消除 zenwebp 调用与
into_dimensions 逻辑的双份实现。
附带收紧:read_dimensions_from_bytes 原用 with_guessed_format() 猜测
格式,现改为按扩展名显式匹配,避免猜测错误。
image_reader_limits() 改为 pub(crate),upload.rs 的内联 limits 构造
(4 行逐字重复)改为调用共享函数。
2026-07-14 15:13:21 +08:00
fade4e180f
refactor: 删除死代码 CommentActions 组件
...
components/comments/actions.rs (CommentActions) 零引用——评论管理
操作(通过/垃圾/删除)已在 pages/admin/comments.rs 用 BTN_SOLID_*
常量实现,actions.rs 是遗留的重复实现。
原计划将它的内联按钮样式提取为 BTN_PILL_* 常量统一,但确认零引用后
直接删除整个文件更干净。trash_comment re-export 补 allow(unused_imports)
(wasm-only import 在 server 构建触发的已知模式)。
2026-07-14 15:09:41 +08:00
9e57247a2d
refactor: 统一 WASM sleep 到 utils::time::sleep_ms
...
删除 system.rs 的私有 wasm_sleep(与 utils::time::sleep_ms 逐字相同),
6 处调用改用 crate::utils::time::sleep_ms。
ui.rs 的内联 Promise+setTimeout(50ms) 同样替换为 sleep_ms(50)。
消除 3 份 sleep 实现中的 2 份冗余副本,utils::time::sleep_ms 成为全项目
唯一的 sleep 入口(已有 native tokio + WASM 双实现)。
2026-07-14 14:55:34 +08:00
804bcfb7b7
refactor(upload): 抽取 upload_error() 消除 15 处 JSON 错误响应样板
...
upload.rs 中 14 处 return Err((StatusCode::X, Json(json!({"success":
false, "error": MSG})))) + 1 处 map_err 同款 tuple,全部替换为
upload_error(status, msg) 单行调用。helper 接受 impl Serialize,
兼容 &str / String / 字面量等调用形式。
2026-07-14 14:53:18 +08:00
8da678b3b4
refactor(cache): 抽取 invalidate_post_metadata() 封装 4 行失效组合
...
文章写操作后总要一起失效 lists/tags/stats/search 四项元数据缓存。
新增 invalidate_post_metadata() 封装这个固定组合,替换 create/delete/
update/trash 中的 8 处重复 4 行序列。
保留定向失效(invalidate_post_by_slug / invalidate_tag_posts_for)
和批量回退路径(invalidate_all_post_caches + invalidate_search_results)
的显式调用——它们是按实际涉及数据细粒度控制的,不应被封装吞掉。
2026-07-14 14:44:00 +08:00
22750ef89e
refactor(api): 为 Response 类型添加构造器,消除 51 处样板
...
CreatePostResponse 新增 err()/ok()/ok_msg() 构造器,消除 posts/
下 34 处 { success, message, post_id, slug } 字面量构造。
CommentResponse 新增 error()/ok()/created() 构造器,消除 comments/
下 17 处 { success, message, error_code, comment_id, ... } 字面量构造。
构造器只在 server function body 内调用,WASM 端因 cfg gate 剥离了
调用点,故加 #[cfg_attr(not(feature="server"), allow(dead_code))]。
行为完全不变(505 测试全过),代码量净减约 300 行样板。
2026-07-14 14:36:56 +08:00
16caa7dd9e
fix(comments): 统一 escape_html,修复代码块单引号未转义
...
comments/markdown.rs 的本地 html_escape 只转义 4 个字符(漏单引号),
与规范实现 utils::html::escape_html(5 字符,'→')不一致。
删除本地副本,改用 crate::utils::html::escape_html。评论代码块里的
单引号现在会被正确转义,与其他上下文的转义行为统一。
2026-07-14 14:29:53 +08:00
b39afbb616
refactor: 删除死代码 (delayed_loading.rs / ui.rs EmptyState / 未用 re-export)
...
- 删除 src/hooks/delayed_loading.rs:未在 hooks/mod.rs 注册,已被
DelayedSkeleton 组件取代(mod.rs 注释已说明)
- 删除 src/components/ui.rs 的 EmptyState:零调用者,所有实际用法
都走功能更全的 components/empty_state.rs::EmptyState
- 删除 api/posts/mod.rs 末尾的 render_markdown_enhanced / slug 工具
re-export:调用方均使用完整路径 crate::api::{markdown,slug}::*,
这两个跨模块短路径无引用
- 删除 api/comments/mod.rs 的 render_comment_markdown re-export:
调用方用完整路径 crate::api::comments::markdown::render_comment_markdown
保留 auth.rs::invalidate_user_sessions:有完整文档说明的预留基础设施
(角色变更/封禁场景),非遗留垃圾。
2026-07-14 14:27:31 +08:00
0e4109358f
docs(system): 修复 BackupRowProps 文档里 Popover 的 broken intra-doc link
...
CI / check (push) Failing after 11m58s
CI / build (push) Has been skipped
[`Popover`] 简写形式解析失败——Popover 的 use 只在 fn BackupRow 函数体里,
对 BackupRowProps struct 的 doc comment 不可见。补全为全路径
[`Popover`](crate::components::ui::Popover),保留可点击跳转。
2026-07-14 13:39:34 +08:00
eccaa9f2a9
chore: release v0.4.0
...
Code Runner + /admin/system + UI 重新设计
v0.4.0
2026-07-13 19:59:22 +08:00
c3ae0aa975
refactor(skills): deploy-to-xun → deploy-to-linux 通用化
...
将部署 skill 从 xun 专属改为适用于任意 Linux 服务器:
- 新增「第 0 步:探测目标服务器」:一条 ssh 命令拿全 OS/运行时/shell/
socket/磁盘/端口/容器列表,配套决策表
- 新增「Docker vs Podman 关键差异」表(socket 路径/镜像短名/tmpfs 兼容)
- 前置反代从「复用 nginx-proxy」单一选项扩展为三方案决策:
A 复用现有 nginx-proxy / B 自建 nginx-proxy / C 自建 nginx + 手动证书
- compose 模板参数化:<host>/<域名>,socket 挂载按 Docker/Podman 二选一
- 保留项目专属部分(LANGUAGES 注册表硬编码、runner 沙箱、镜像名约定)
- fish shell 陷阱、Rosetta 要求、buildx 命名 bug、scratch 健康检查等
实战经验全部保留
2026-07-13 19:47:56 +08:00
7584e0ca1c
docs(skills): 新增 deploy-to-xun 部署流程 skill
...
将本次实战验证的部署流程提取为可复用 skill,涵盖:
- 本地 arm64 用 Docker Rosetta 构建 x86 镜像(绕过 QEMU 跑 rustc 的 SIGSEGV)
- runner 镜像 localhost/ 前缀与 LANGUAGES 注册表硬编码名的 tag 映射
- xun 服务器 fish shell 陷阱(变量赋值/循环/退出码语法差异)
- 复用 nginx-proxy + acme-companion 自动签证的 compose 配置
- podman socket 路径映射与 scratch 镜像健康检查方式
- Code Runner 在 podman 上的 tmpfs 兼容性已知限制
每一步均在本次会话中实战验证通过(14 迁移成功、HTTPS 200、证书签发)。
2026-07-13 19:40:41 +08:00
067075df8a
fix(post): 直接访问 #hash 时标题被 sticky header 遮住
...
点击锚点正常,但直接访问带 hashtag 的链接时滚动后标题被 80px sticky
header 遮住。根因:浏览器原生 fragment-scroll(直接访问触发)不读 CSS
scroll-margin-top,在我们 scrollToHash 的平滑滚动之后触发,把标题顶到
视口最顶端(header 下方)。
抽出共享 scrollToHeading:用 getBoundingClientRect 手动计算绝对滚动位置,
减去动态测量的 header 高度(+16px 呼吸空间)后 window.scrollTo,不再依赖
scroll-margin-top。hash-scroll(直接访问)与 anchor-click(点击)统一调用它。
scrollToHash 额外用双 requestAnimationFrame 延后做一次即时校正,覆盖原生
fragment-scroll 的触发窗口,确保落点在原生 scroll 之后生效。
scroll-margin-top: 6rem 保留在 input.css 作其它原生 scroll 场景的兜底。
2026-07-13 18:31:31 +08:00
5ad6ce60cd
fix(post): hydration 后点击标题锚点触发整页刷新
...
WASM 下载完成后点击 TOC/标题 # 锚点会整页刷新且不滚动,而 SSR 阶段(WASM
未就绪)点击正常——这是 Dioxus 0.7 事件委托的副作用:
项目任意位置用 onclick(post_nav_links.rs)即触发 Dioxus 全局 click 委托。
点击 dangerous_inner_html 注入的 <a href="#id"> 时,事件冒泡到根监听器,
handleEvent 发现 <a> 无 Dioxus onclick → 走兜底 handleClickNavigate:
preventDefault 拦掉原生 fragment-scroll,再 browser_open IPC 把 hash 当
外部 URL 整页加载(History::external → location.set_href)。
修复:yggdrasil-core 新增 anchor-click.ts,在 window capture 阶段(早于
Dioxus 的 bubble 委托)拦截同页 hash 锚点点击,stopPropagation 阻止事件
到达 Dioxus,自行 scrollIntoView + replaceState 更新地址栏。中键/修饰键、
外链、目标不存在等情况放行原行为。
81682eb 的 scrollToHash 只在 PostContent 挂载时跑一次,不覆盖点击场景;
本修复与之正交,共同覆盖刷新与点击两条路径。initAnchorClick 幂等。
2026-07-13 18:22:53 +08:00
56f599574f
feat(slug): 中文标题自动转拼音生成 URL slug
...
文章 URL slug 与 markdown 标题锚点此前会吃掉中文字符:纯中文标题
退化成时间戳,混入 ascii 时只剩 ascii 片段。
引入 pinyin crate(纯 Rust、零依赖,安全交叉编译到 musl/FreeBSD),
slugify 与 slugify_heading 在字符过滤前先尝试 to_pinyin():
- 汉字 → 无声调拼音,每字成词用 - 分隔(你好 → ni-hao)
- ASCII 字母数字与 -/_ 保留
- 多音字取默认读音,博客场景足够
示例:你好世界 → ni-hao-shi-jie;Rust 入门指南 → rust-ru-men-zhi-nan
is_valid_slug 不变(原本就允许 Unicode),手动输入中文 slug 仍可用。
pinyin 为 server-only optional 依赖,不进 WASM bundle。
2026-07-13 18:04:35 +08:00
cc4c551aaa
fix(code-runner): 容器清理失败重试+日志告警,避免静默泄漏
...
ContainerGuard::drop 之前用 let _ = 吞掉 remove_container 的错误,
Docker daemon 瞬时不可用会让容器泄漏且无人知晓。
改为最多重试 3 次(指数退避 200/400/800ms),仍失败则记录 error 级日志
(带 container_id 便于 docker rm -f 兜底)。容器清理是 fire-and-forget,
无法把错误回传业务层,所以用日志暴露故障而非 panic。
2026-07-13 17:56:42 +08:00
ee9d2ecd6e
style(ui): 组件圆角类对齐三档梯度(32/16/8)
...
配合 input.css 的 token 化,把散落在 RSX 组件里的孤悬圆角档位
统一到内容档 16px (rounded-2xl):
消除 24px (rounded-3xl, 4处无规范依据的中间档):
- ADMIN_CARD_CLASS / ADMIN_TABLE_CLASS (ui.rs)
- write.rs 编辑器容器 / write_skeleton.rs 编辑器骨架
消除 12px (rounded-xl, 10处):
- INPUT_CLASS (forms.rs) 与 write 页输入框统一到 16px
- write 页 3 处输入框 + 4 处错误提示框
- admin_layout 退出按钮
- system/admin_skeleton/posts_trash 卡片容器
- write_skeleton 骨架条(模拟输入框形状,跟随真实圆角)
改造后全站只剩 4 种圆角语义: 32px容器 / 16px内容 / 8px微元素 /
胶囊按钮,孤悬档位彻底清除。
2026-07-13 16:21:10 +08:00
248dcb761c
refactor(ui): 圆角统一为三档梯度(32/16/8)+token化
...
将全站圆角收敛为语义化三档梯度:
- --radius-card 32px 容器(卡片/封面/admin主面板)
- --radius-paper 16px 内容(代码块/正文图片/iframe/表格)
- --radius-mini 8px 微元素(标签/复制按钮/toc/paginav)
此前内容区代码块、正文图片圆角仅 8px,与卡片 32px 设计语言断层
4 倍;表格 16px 是硬编码字面量散落 5 处。本次:
- --radius-paper 从 8px 提到 16px,代码块/图片/iframe 随之提档
- 表格 5 处 16px 字面量改用 var(--radius-paper)(值不变,语义化)
- toc/复制按钮/标签/paginav/blur-img 拆出到新 --radius-mini:8px
- 封面 2rem 改用 var(--radius-card)(值不变,语义化)
按钮/头像/徽章另用胶囊(rounded-full),独立于本梯度,不动。
2026-07-13 16:21:02 +08:00
2310ed9abe
fix(system): 备份恢复实际不写入数据 + 假成功
...
三层叠加 bug 导致「恢复完成却零数据变更」:
1. 备份用 pg_dump 生成但无 --clean,脚本不含 DROP。恢复到已有数据的库时,
CREATE TABLE 全部 'already exists',COPY public.posts 在第一行就因主键
冲突中止(COPY 0),被软删的文章(deleted_at 非空)永远回不来。
2. psql 默认不带 ON_ERROR_STOP,即使满屏 ERROR 退出码仍是 0。run_restore
只判断 status.success(),于是 tasks::update 误报 TaskStatus::Done——
用户看到「恢复完成」但实际零写入。
3. 即使数据写进去,前端 moka 缓存(posts/tags/stats/search)和 SSR 世代号
也不会失效,恢复后仍读旧数据。
修复:
- run_pg_dump_backup 加 --clean --if-exists,备份脚本自带 DROP IF EXISTS,
恢复变为幂等的先删后建。
- run_restore 给 psql 加 -v ON_ERROR_STOP=1,任何 SQL 错误立即退出(码 3),
status.success() 不再误判。
- 恢复成功后调用 invalidate_all_post_caches + invalidate_search_results
+ bump_global_generation,前端立即看到恢复的数据。
验证:建干净测试库跑完整反馈回路(3 篇 → 软删 2 篇 → 恢复),修复前 alive=1,
修复后 alive=3,exit 0。旧备份文件(无 DROP)在 ON_ERROR_STOP 下会正确报失败
而非假成功,需重新创建备份才能恢复。
根因假设(已通过最小反馈回路证实):pg_dump 无 --clean + psql 无 ON_ERROR_STOP +
缓存未失效三层叠加。hypothesis 来自 backup.rs 源码 + backups/*.sql 实际结构 +
psql 退出码语义实测。
2026-07-13 15:57:34 +08:00
5888f1fc41
feat(system): 备份恢复删除/恢复改用 Popover 确认框替代浏览器 confirm
...
新增通用 Popover 组件(src/components/ui.rs,与 Tooltip 对称),定位用
position:fixed + MouseEvent::client_coordinates() 视口坐标锚定,逃出表格
overflow-hidden;透明遮罩 z-40 兜底点击外部关闭,组件内 use_effect 注册
全局 keydown 监听 Esc 关闭(use_drop 清理),150ms 淡入缩放动画
(input.css 新增 popover-enter keyframes)。
BackupRow 删除/恢复按钮 onclick 读点击坐标打开对应确认 popover,确认
按钮回调父组件。顺带简化恢复链路:原生 confirm 是阻塞式才需要
pending_restore signal + 确认 use_future 的间接机制,popover 非阻塞后
on_restore 直接 busy + restore_backup + active_task_id,移除 pending_restore
signal 及其 use_future(进度轮询 use_future 不变,仍服务创建备份+恢复)。
2026-07-13 15:08:24 +08:00
c1530956b0
style(system): 数据导出 tab 卡片宽度与其它 tab 对齐
...
ExportTab 根容器带了 max-w-2xl 把卡片限宽在 672px,而 DbStatusTab /
ServerStatusTab / SqlConsoleTab / BackupTab 都不限宽填满 max-w-7xl
内容区。去掉 max-w-2xl 让数据导出卡片与其它 tab 宽度一致。内部表单
输入用 w-full,加宽后自然拉伸,无空白。
2026-07-13 14:23:14 +08:00
2e40836480
fix(system): 备份/恢复任务轮询永不启动导致按钮卡死在 loading
...
点击「创建备份」后接口返回 task id,但按钮一直转圈、列表不刷新。
根因是 AGENTS.md 踩坑记录里的「Reactive hooks 不追踪普通 props/snapshot」:
进度轮询 use_future 在挂载时把 active_task_id 快照进 _task_id_for_poll
(彼时为 None),use_future 只运行一次即 return;用户点击后 create_backup
返回 task id 并 active_task_id.set(Some(id)),但 future 已结束、永不重跑
→ 轮询不启动 → busy 永远 true。恢复确认 use_future 有同样的快照陷阱
(_pending_for_confirm),导致点「恢复」连确认框都不弹。
改用与 DbStatusTab 自动刷新一致的长生命周期 loop 模式:use_future 只跑
一次,内部 loop 每轮迭代读 active_task_id() / pending_restore() 信号,
空闲时 200ms yield 呼吸。这样信号更新在下一轮迭代即被感知,无需依赖
ReactiveContext 对挂载期快照的订阅。
2026-07-13 13:56:22 +08:00
cf14aae17f
style(ui): 统一 admin 分页按钮为圆角胶囊样式
...
admin 分页的上一页/下一页按钮原先用 rounded-sm 方角实心(灰黑底
+ font-mono),与本页其它操作按钮(BTN_OUTLINE 等圆角胶囊)风格
不一致。改为与描边按钮同族的 rounded-full 胶囊,hover 走主题强调
色,禁用态同步圆角。
因三个 admin 列表页(posts / posts_trash / comments)共用
Pagination 组件的 admin variant,此改动一次性统一全部后台分页。
2026-07-13 13:38:50 +08:00
093940a0d7
fix(theme): skip editor set_theme during VT animation to prevent direct jump
...
主题切换时,可运行代码块(CodeMirror + xterm)整体瞬切,不受 VT 圆形展开动画
控制——圆形还没展开到代码块,代码块就整体变色了。
根因(真实页面逐帧像素采样 + 事件追踪确认):
ThemeToggle::onclick 先调 __startThemeTransition(同步截 OLD 快照,VT 回调异步),
再调 theme.set(next)。theme.set 触发 Dioxus use_effect → set_theme,这个调用
在 OLD 快照截取后、VT 回调执行前的时间窗内,直接改了实时 DOM 的编辑器背景。
VT 动画播的是伪元素快照(::view-transition-old/new),但实时 DOM 的改动会穿透
伪元素,表现为代码块在圆形展开到达前就整体瞬切。
上一个提交(theme-change 事件)已在 VT 回调内同步 setTheme(进入 NEW 快照),
但 use_effect 的冗余 set_theme 仍在动画期间改实时 DOM,抵消了快照内的正确状态。
修复:CodeMirror + xterm 的 use_effect 在 is-theme-transitioning 期间跳过
set_theme 调用。VT 事件已在快照前同步换肤;动画结束后 is-theme-transitioning
被移除,use_effect 会因 resolved 信号变化重跑,做幂等兜底同步(覆盖非 VT 场景:
系统偏好变化、初始挂载)。
验证(scripts/vt-editor-sweep.mjs,真实 Dioxus 点击路径):
- 修复前:编辑器整体瞬切(t=111ms 全部变 dark,无渐变)
- 修复后:编辑器从右向左被圆形逐步揭示(t=228ms 右侧先 dark,t=313ms 左侧 dark,
中间点显示圆形边缘抗锯齿色),与页面其他部分同步
2026-07-13 13:33:33 +08:00
f984c0c3e7
fix(theme): clear animate-page-enter transform during VT to fix code block coverage
...
主题切换的圆形展开 VT 动画对可运行代码块不生效:代码块直接瞬切,而非被
圆形展开揭示。上一个提交(theme-change 事件同步 setTheme)只解决了快照时序
问题,但真实环境仍不生效。
根因(真实页面逐帧截图 + 像素采样确认):article.post-single 的 animate-page-enter
入场动画用 transform: translateY/scale,因 animation-fill-mode: both 在动画结束后
保留计算值。transform: none 被浏览器序列化为 matrix(1,0,0,1,0,0),仍是「非 none」
的 transform,创建堆叠上下文(stack context)。::view-transition-old/new(root) 顶层
伪元素无法正确覆盖堆叠上下文内部的 CodeMirror/xterm——圆形展开扫过代码块区域时,
伪元素被穿透,代码块的实时 DOM 变化(use_effect 驱动的 setTheme)直接可见,表现为
「直接瞬切,不受 VT 动画控制」。
修复:在 is-theme-transitioning 期间用 !important 清除 .animate-page-enter 的
transform。is-theme-transitioning 在 startViewTransition 之前添加,OLD/NEW 快照
均含此 class,故两快照 transform 都被清为 none,一致,不影响动画效果。
验证:
- scripts/vt-real-page-sampler.mjs + vt-xterm-real-sampler.mjs:Playwright 驱动
真实 dx serve 页面,逐帧截图 + 像素采样
- 修复前:代码块在圆形边界外已变 dark(直接瞬切);修复后:代码块随圆形展开
同步变色(lag 与几何距离一致,非瞬切)
- 像素追踪:editor 在 t=266ms 变色(圆形到达),bg 在 t=362ms 变色(圆形到达更远点),
时序与圆形几何一致,证明两者都通过 VT 动画变色
2026-07-13 12:02:30 +08:00
bbb77954e9
fix(theme): sync CodeMirror/xterm theme in VT callback for circular-expand animation
...
主题切换的圆形展开 VT 动画对可运行代码块不生效:圆形扫过 CodeMirror/xterm
区域时看不到颜色变化,动画结束后才瞬切。
根因(逐帧像素验证确认):VT 回调只 toggle .dark class,但 CodeMirror 背景
由 catppuccin Extension 注入(.cm-editor 的 EditorView.theme 规则),xterm
背景由 .xterm-scrollable-element 的 inline background-color 注入——两者都
不随 .dark 翻转。set_theme 由 Dioxus use_effect 在 theme.set() 之后异步触发,
晚于 NEW 快照捕获,导致 OLD/NEW 快照在编辑器区域同色,圆形展开无可揭示的变化。
修复:在 VT 回调内(NEW 快照捕获前)同步 dispatch 'yggdrasil:theme-change'
CustomEvent,CodeMirror/xterm 各自的 index.ts 在模块加载时订阅并遍历
_instances 同步调 setTheme。事件先于 applyDarkClass dispatch,确保编辑器换肤
+ class 翻转被同一个 getComputedStyle reflow 捕获进 NEW 快照。
与现有 Dioxus use_effect 幂等共存:setTheme 对相同主题是 no-op(CodeMirror
Compartment.reconfigure + xterm options.theme = 均幂等),use_effect 作兜底
覆盖初始挂载 / 非 VT 场景。
验证:
- yggdrasil-core 测试扩展:断言 VT 回调 / 降级路径 / applyResolvedTheme 都
dispatch 事件,且事件先于 dark class 翻转
- scripts/vt-theme-sampler.mjs(Playwright 逐帧像素采样):修复前 xterm 比
等距的 cssvar 探针晚 100ms 变色(bug);修复后同帧变色(lag=0ms)
改动文件:
- libs/yggdrasil-core/src/theme-transition.ts: THEME_CHANGE_EVENT 常量 +
notifyThemeChange 辅助函数,VT 回调 / 降级路径 / applyResolvedTheme 三处 dispatch
- libs/codemirror-editor/src/index.ts: 订阅事件,forEach _instances.setTheme
- libs/xterm-terminal/src/index.ts: 同样模式
- libs/yggdrasil-core/src/theme-transition.test.ts: 3 个新用例
- public/xterm/terminal.js: 重建产物(含事件订阅)
- scripts/vt-theme-{harness,sampler}: 逐帧像素验证工具(throwaway,可作回归)
2026-07-13 11:20:08 +08:00
e50941ef24
fix(runner): propagate duration_ms through SSE done event
...
The SSE streaming path hardcoded '耗时: -' because duration_ms was
never threaded through:
- docker.rs: OutputChunk::Done now carries duration_ms, computed from
start_container to wait completion via Instant::elapsed
- sse.rs: DonePayload gains duration_ms, serialized into the done event
- runner.rs: WASM done callback formats the real duration instead of '-'
The polling fallback path already had correct duration via ExecResult;
only the SSE path was missing it.
2026-07-13 10:09:15 +08:00
aa2d7f3f2d
fix(runner): use python -u for unbuffered stdout streaming
...
Python defaults to 4KB block buffering when stdout is a pipe (not a
TTY). Docker attach uses pipes, so print() output accumulated in the
buffer and only flushed when the process exited — the 3-second
sleep(1) loop appeared as a single dump at the end.
python -u (equivalent to PYTHONUNBUFFERED=1) forces line-level
flushing, so each print() is immediately written to the pipe and
picked up by the concurrent log reader → SSE → xterm.js.
Node/Go/Rust already line-buffer to pipes, so only Python needed the
flag. stdout/stderr separation is preserved (no TTY merge).
2026-07-13 10:03:58 +08:00
2059a55e11
fix(runner): make wait_container concurrent with log reading
...
run_in_container_stream was sequentially awaiting wait_container before
reading the log stream. wait_container blocks until the container
exits, so by the time the log loop ran, all output was already buffered
in the attach stream and got drained near-instantly — appearing as a
one-shot dump instead of streaming.
Now uses tokio::select! to run log_reader and wait_with_timeout
concurrently:
- log_reader: drains the attach stream, pushing each chunk via tx
immediately as it arrives
- wait_with_timeout: waits for container exit (with timeout, kills on
timeout)
Whichever finishes first wins; the survivor is drained/handled. When
wait completes, any tail bytes still in the stream are flushed before
sending the Done chunk.
This is the root cause of the streaming not working — a sleep(1) loop
now streams line-by-line instead of dumping all at once after exit.
2026-07-13 09:59:03 +08:00
9191d07200
fix(ui): hide output area until first run; show skeleton while waiting
...
The output area was always visible (empty terminal container shown on
page load). Now it only appears after the user clicks Run, with three
states:
- Not run: output area hidden entirely (show_output = false)
- Running, no output yet: skeleton screen placeholder
(running && !has_output)
- Output received: skeleton gone, xterm renders streamed content
Two new signals drive this:
- show_output: set true on Run click, controls output area visibility
- has_output: set true on first stdout/stderr chunk (SSE) or writeAll
(polling fallback), dismisses the skeleton
xterm mount effect now reads show_output, so it retries after the
container div enters the DOM (when show_output flips true).
2026-07-13 09:49:57 +08:00
e347776dcd
fix(ui): align xterm terminal background with --color-paper-code-block
...
xterm.js terminal background was Catppuccin Base (#eff1f5 / #1e1e2e),
but the project's code-block background CSS variable uses Catppuccin
Surface0 (#dce0e8 light / #313244 dark). The mismatch left the output
area visibly lighter (light mode) / darker (dark mode) than the
surrounding container.
2026-07-13 09:43:01 +08:00
6f4385221a
test(libs): add xterm-terminal smoke tests
...
4 tests covering XtermOptions construction/setters and TerminalInstance
mount/onReady/writeAll/clear/destroy lifecycle under happy-dom.
2026-07-10 11:59:38 +08:00
7d153e50f4
feat(ui): CodeRunner switches to SSE + xterm.js streaming output
...
WASM path: start_exec_stream → EventSource SSE → xterm.js terminal.
stdout/stderr events write to the terminal in real time; done event
sets exit status and closes the connection. Falls back to polling
get_exec_result (writeAll) if EventSource creation fails.
Server path (placeholder, component runs client-side): retains the
original polling logic so both targets compile.
- xterm.js terminal mounted via xterm_bridge (mirrors CodeMirror mount
pattern: use_effect + TerminalHandle + use_drop)
- run_code split into #[cfg(wasm32)] (SSE) and #[cfg(not)] (polling)
- output area: <pre> replaced with xterm container div
- poll_result / start_sse helpers in WASM-only sse_consumer module
- term_handle declared at component scope (cfg-gated) so the WASM
run_code closure can capture it
Note: dx check reports use_resolved_theme-in-closure for the new xterm
effects, same as the pre-existing CodeMirror effects (lines 145, 177).
This is an existing project-wide pattern; cargo build on both targets
and cargo clippy pass clean.
2026-07-10 11:58:25 +08:00