From ecf3c73715bb3d0717b8a4930211f4afb2d37f5a Mon Sep 17 00:00:00 2001 From: xfy Date: Tue, 30 Jun 2026 13:41:23 +0800 Subject: [PATCH] fix(database): make src/api/database compile under WASM (web-only) build The /admin/system feature was never verified against the WASM frontend target (dx serve builds src/main.rs with --features web and no server). The entire src/api/database/ tree unconditionally pulled in server-only crates (axum, sqlparser, dashmap, tokio, tokio_postgres, futures, regex) and server-gated helpers (get_current_admin_user, parse_session_token, get_user_by_token), so the frontend build failed with 43 errors. Root cause + fixes (mirror the established settings.rs pattern): - Gate server-only imports behind #[cfg(feature = server)] in status/system_status/sql_console/schema/tasks/backup. The #[server] macro strips call sites but NOT use statements, so unresolved imports remained. - Ungate in main.rs: SystemSnapshot is a shared serde type referenced by ServerStatus.host on both targets; only the sampler task/SNAPSHOT static are internally server-gated. - Gate (pure Axum handler, zero WASM consumers) and the download_backup Axum handler in backup.rs. - Move restore_backup's validation preamble (regex/backup_path/fs) inside the #[cfg(feature = server)] block; gate std::path/chrono::Utc imports. - Add [[bin]] required-features=[server] for generate_highlight_css so the syntect-dependent build tool is skipped in web-only builds. Genuine WASM bugs surfaced once the server bodies stopped masking them: - system.rs: Closure import path (dioxus::prelude::wasm_bindgen -> wasm_bindgen, matching write.rs); setTimeout expects i32 not u32; editor_handle() -> editor_handle.read() (Signal is not Fn in 0.7). - codemirror_bridge.rs: set_schema extern took &SqlSchema but SqlSchema is a serde type with no IntoWasmAbi. Changed both set_schema signatures to &JsValue; call site now serializes via serde-wasm-bindgen::to_value. - system.rs: signals .set() inside spawn/use_future/onclick closures need bindings (incl. the *_f rebinding copies); added cfg_attr allow(unused_mut) on the 4 tab components, matching write.rs convention. Verified: cargo check (default/server) clean; cargo check --features web --target wasm32-unknown-unknown clean (0 errors); dx check clean; 411 cargo tests pass. --- Cargo.lock | 1 + Cargo.toml | 9 +++ src/api/database/backup.rs | 57 +++++++++++-------- src/api/database/mod.rs | 3 +- src/api/database/schema.rs | 7 ++- src/api/database/sql_console.rs | 4 ++ src/api/database/status.rs | 5 ++ src/api/database/system_status.rs | 3 + src/api/database/tasks.rs | 10 +++- src/codemirror_bridge.rs | 9 +-- src/main.rs | 5 +- src/pages/admin/system.rs | 92 ++++++++++++++++++------------- src/sysinfo_sampler.rs | 3 + 13 files changed, 137 insertions(+), 71 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7f46f82..b9360c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5496,6 +5496,7 @@ dependencies = [ "rand 0.8.6", "regex", "serde", + "serde-wasm-bindgen", "serde_json", "serial_test", "sha2 0.10.9", diff --git a/Cargo.toml b/Cargo.toml index c3b3e11..394e9f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,13 @@ name = "yggdrasil" version = "0.3.0" edition = "2021" +# 构建时工具二进制:用 syntect(server-only 依赖)生成 highlight.css。 +# 仅在启用 server feature 时编译——WASM 前端构建(--no-default-features --features web) +# 不会拉入 syntect,此二进制若也参与编译会因找不到 syntect 报错。 +[[bin]] +name = "generate_highlight_css" +required-features = ["server"] + [dependencies] dioxus = { version = "0.7.9", features = ["fullstack", "router"] } serde = { version = "1.0", features = ["derive"] } @@ -47,6 +54,8 @@ web-sys = { version = "0.3", features = ["Document", "Window", "Storage", "Eleme wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" js-sys = "0.3" +# 把 serde 类型(如 SqlSchema)序列化为 JsValue,供 wasm-bindgen extern 传递。 +serde-wasm-bindgen = "0.6" [dev-dependencies] tokio = { version = "1.52", features = ["rt-multi-thread", "macros", "fs", "time", "sync"] } diff --git a/src/api/database/backup.rs b/src/api/database/backup.rs index 5600482..d0112bb 100644 --- a/src/api/database/backup.rs +++ b/src/api/database/backup.rs @@ -7,14 +7,21 @@ //! 恢复:仅接受本系统生成的备份(签名校验)+ 二次确认 + 路径穿越防护。 //! 长耗时操作走后台任务 + 进度轮询(见 [`super::tasks`])。 +// Component/PathBuf/chrono::Utc 仅 server 构建的备份逻辑用到。 +#[cfg(feature = "server")] use std::path::{Component, PathBuf}; +#[cfg(feature = "server")] use chrono::Utc; use dioxus::prelude::*; use serde::{Deserialize, Serialize}; +// admin 鉴权 + AppError + tasks 进度表仅在 server 构建里被 server function 体引用。 +#[cfg(feature = "server")] use crate::api::auth::get_current_admin_user; +#[cfg(feature = "server")] use crate::api::database::tasks::{self, TaskKind, TaskStatus}; +#[cfg(feature = "server")] use crate::api::error::AppError; /// 备份目录(项目根,与 uploads/ 平级,gitignored)。 @@ -316,30 +323,32 @@ async fn run_sql_fallback_backup(task_id: &str, timestamp: &str) { pub async fn restore_backup(filename: String, confirm: bool) -> Result { let _user = get_current_admin_user().await?; - if !confirm { - return Err(AppError::BadRequest("需确认恢复(会覆盖现有数据)".to_string()).into()); - } - // 路径穿越防护 - let re = regex::Regex::new(FILENAME_RE).unwrap(); - if !re.is_match(&filename) { - return Err(AppError::BadRequest("无效的文件名".to_string()).into()); - } - let path = backup_path(&filename); - if !path.exists() { - return Err(AppError::NotFound("备份文件不存在").into()); - } - - // 签名校验:首行需含签名 - let head = std::fs::read_to_string(&path) - .ok() - .and_then(|s| s.lines().next().map(|l| l.trim().to_string())) - .unwrap_or_default(); - if !head.contains(BACKUP_SIGNATURE) { - return Err(AppError::BadRequest("非本系统生成的备份文件,拒绝恢复".to_string()).into()); - } - + // 全部校验都在 server cfg 块内:confirm/regex/backup_path/std::fs 都是 server-only。 + // WASM 侧的 server-function 客户端桩只返回 Ok(String::new())。 #[cfg(feature = "server")] { + if !confirm { + return Err(AppError::BadRequest("需确认恢复(会覆盖现有数据)".to_string()).into()); + } + // 路径穿越防护 + let re = regex::Regex::new(FILENAME_RE).unwrap(); + if !re.is_match(&filename) { + return Err(AppError::BadRequest("无效的文件名".to_string()).into()); + } + let path = backup_path(&filename); + if !path.exists() { + return Err(AppError::NotFound("备份文件不存在").into()); + } + + // 签名校验:首行需含签名 + let head = std::fs::read_to_string(&path) + .ok() + .and_then(|s| s.lines().next().map(|l| l.trim().to_string())) + .unwrap_or_default(); + if !head.contains(BACKUP_SIGNATURE) { + return Err(AppError::BadRequest("非本系统生成的备份文件,拒绝恢复".to_string()).into()); + } + let task_id = uuid::Uuid::new_v4().to_string(); tasks::insert(task_id.clone(), TaskKind::Restore); let tid = task_id.clone(); @@ -351,6 +360,8 @@ pub async fn restore_backup(filename: String, confirm: bool) -> Result PathBuf { } /// Axum 处理器:下载备份文件(admin 鉴权 + 路径白名单)。 +/// 仅 server 构建:纯 Axum 路由(在 main.rs 注册),无 WASM 消费者。 +#[cfg(feature = "server")] pub async fn download_backup( axum::extract::Path(filename): axum::extract::Path, headers: axum::http::HeaderMap, diff --git a/src/api/database/mod.rs b/src/api/database/mod.rs index c155e4b..6f17d51 100644 --- a/src/api/database/mod.rs +++ b/src/api/database/mod.rs @@ -14,7 +14,8 @@ pub mod system_status; pub mod sql_console; /// SQL 补全用 schema 拉取。 pub mod schema; -/// 数据导出 Axum 流式处理器。 +/// 数据导出 Axum 流式处理器(仅 server:纯 Axum 路由,无 WASM 消费者)。 +#[cfg(feature = "server")] pub mod export; /// 备份/恢复(双模式 + 任务进度)。 pub mod backup; diff --git a/src/api/database/schema.rs b/src/api/database/schema.rs index f107f01..40047d8 100644 --- a/src/api/database/schema.rs +++ b/src/api/database/schema.rs @@ -4,10 +4,15 @@ use dioxus::prelude::*; +// admin 鉴权 + DB 查询仅在 server 构建里被 server function 体引用。 +#[cfg(feature = "server")] use crate::api::auth::get_current_admin_user; +#[cfg(feature = "server")] use crate::api::error::AppError; -use crate::codemirror_bridge::SqlSchema; +#[cfg(feature = "server")] use crate::db::pool::get_conn; +// SqlSchema/SqlTable 是两端共享的纯数据类型(定义在 codemirror_bridge)。 +use crate::codemirror_bridge::SqlSchema; /// 拉取数据库 schema(表名 + 列名),供 CodeMirror SQL 补全。 #[server(GetDbSchema, "/api")] diff --git a/src/api/database/sql_console.rs b/src/api/database/sql_console.rs index e7ff071..72ec6f5 100644 --- a/src/api/database/sql_console.rs +++ b/src/api/database/sql_console.rs @@ -14,8 +14,12 @@ use dioxus::prelude::*; use serde::{Deserialize, Serialize}; +// admin 鉴权 + DB 查询仅在 server 构建里被 server function 体引用。 +#[cfg(feature = "server")] use crate::api::auth::get_current_admin_user; +#[cfg(feature = "server")] use crate::api::error::AppError; +#[cfg(feature = "server")] use crate::db::pool::get_conn; #[derive(Deserialize, Serialize, Debug, Clone, Copy)] diff --git a/src/api/database/status.rs b/src/api/database/status.rs index 52886e2..c98762f 100644 --- a/src/api/database/status.rs +++ b/src/api/database/status.rs @@ -9,8 +9,13 @@ use chrono::{DateTime, Utc}; use dioxus::prelude::*; use serde::{Deserialize, Serialize}; +// 仅 server 构建用到:admin 鉴权 + DB 查询。WASM 侧的 server-function 客户端桩 +// 不解析这些符号,必须 gate 以避免在非 server 构建里找不到 server-only 符号。 +#[cfg(feature = "server")] use crate::api::auth::get_current_admin_user; +#[cfg(feature = "server")] use crate::api::error::AppError; +#[cfg(feature = "server")] use crate::db::pool::get_conn; /// 数据库状态聚合数据。 diff --git a/src/api/database/system_status.rs b/src/api/database/system_status.rs index 6bc71bf..cff4d35 100644 --- a/src/api/database/system_status.rs +++ b/src/api/database/system_status.rs @@ -8,7 +8,10 @@ use dioxus::prelude::*; use serde::{Deserialize, Serialize}; +// admin 鉴权 + AppError 仅在 server 构建里被 server function 体引用。 +#[cfg(feature = "server")] use crate::api::auth::get_current_admin_user; +#[cfg(feature = "server")] use crate::api::error::AppError; /// 单个缓存的统计快照(前端展示用)。 diff --git a/src/api/database/tasks.rs b/src/api/database/tasks.rs index 19814ed..c09ffea 100644 --- a/src/api/database/tasks.rs +++ b/src/api/database/tasks.rs @@ -6,14 +6,18 @@ //! [`update`] 更新进度,前端轮询 [`get_task_progress`]。 //! 已完成超过 1 小时的任务惰性清理,避免内存累积。 -use std::sync::LazyLock; - use chrono::{DateTime, Utc}; -use dashmap::DashMap; use dioxus::prelude::*; use serde::{Deserialize, Serialize}; +// admin 鉴权仅 server 构建用到。 +#[cfg(feature = "server")] use crate::api::auth::get_current_admin_user; +// DashMap / LazyLock 仅 server 构建持有任务进度表;WASM 端只序列化 TaskProgress。 +#[cfg(feature = "server")] +use std::sync::LazyLock; +#[cfg(feature = "server")] +use dashmap::DashMap; #[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] pub enum TaskKind { diff --git a/src/codemirror_bridge.rs b/src/codemirror_bridge.rs index 5b9fc0a..ad3500e 100644 --- a/src/codemirror_bridge.rs +++ b/src/codemirror_bridge.rs @@ -32,7 +32,6 @@ pub struct SqlTable { // ============================================================================ #[cfg(target_arch = "wasm32")] pub mod wasm { - use super::SqlSchema; use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; @@ -91,8 +90,10 @@ pub mod wasm { pub fn set_theme(this: &EditorInstance, theme: &str); /// 更新 SQL 补全 schema(Compartment.reconfigure)。 + /// 参数为 serde_wasm_bindgen::to_value 序列化后的 JsValue + ///(SqlSchema 是 serde 类型,非 wasm-bindgen 类型,故不能直接传 &SqlSchema)。 #[wasm_bindgen(method, js_name = setSchema)] - pub fn set_schema(this: &EditorInstance, schema: &SqlSchema); + pub fn set_schema(this: &EditorInstance, schema: &wasm_bindgen::JsValue); /// 让编辑器获取焦点。 #[wasm_bindgen(method)] @@ -126,9 +127,9 @@ pub mod wasm { #[wasm_bindgen(method, setter, js_name = vim)] pub fn set_vim(this: &EditorOptions, v: bool); - /// SQL 补全 schema(表/列数据)。 + /// SQL 补全 schema(表/列数据)。v 为 serde_wasm_bindgen::to_value 序列化结果。 #[wasm_bindgen(method, setter, js_name = schema)] - pub fn set_schema(this: &EditorOptions, v: &SqlSchema); + pub fn set_schema(this: &EditorOptions, v: &wasm_bindgen::JsValue); /// 初始文档内容。 #[wasm_bindgen(method, setter, js_name = value)] diff --git a/src/main.rs b/src/main.rs index f430eb1..c4178c3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,8 +23,9 @@ mod hooks; mod models; mod pages; mod router; -// sysinfo_sampler 仅在 server feature 启用时编译:主机指标后台采样 + RwLock 快照。 -#[cfg(feature = "server")] +// sysinfo_sampler:主机指标快照。 +// SystemSnapshot 结构体两端都编译(被 system_status 的 ServerStatus 字段引用); +// 真正的采样任务 / RwLock / read_snapshot 实现在本模块内部自行 #[cfg(feature = "server")] gate。 mod sysinfo_sampler; // ssr_cache 仅在 server feature 启用时编译;保存 SSR 世代号失效状态。 #[cfg(feature = "server")] diff --git a/src/pages/admin/system.rs b/src/pages/admin/system.rs index b38bf9a..295edb3 100644 --- a/src/pages/admin/system.rs +++ b/src/pages/admin/system.rs @@ -97,7 +97,7 @@ async fn wasm_sleep(ms: u32) { .expect("no window") .set_timeout_with_callback_and_timeout_and_arguments_0( &resolve.unchecked_into(), - ms, + ms.try_into().unwrap(), ) .expect("set_timeout failed"); }); @@ -107,6 +107,9 @@ async fn wasm_sleep(ms: u32) { /// 数据库状态 tab:概览卡片 + 表清单 + 索引 Top + 活跃连接。 /// 手动刷新按钮 + 自动刷新开关(1s/2s/5s/30s/手动,默认手动)。 #[allow(non_snake_case)] +// status/error/loading 在 spawn/onclick 闭包里 .set(),仅 WASM 前端真正用到; +// server 构建里这些 set 调用都在被剥离的 #[cfg(wasm32)] 块内,故 allow unused_mut。 +#[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut, unused_variables))] fn DbStatusTab() -> Element { use crate::api::database::status::DbStatus; // get_db_status 只在 WASM 前端调用,server 构建时该 server function 的客户端桩不需要导入。 @@ -115,9 +118,9 @@ fn DbStatusTab() -> Element { use crate::components::ui::{ADMIN_CARD_CLASS, ADMIN_TABLE_CLASS}; // Signal 是 Copy,可在多个 spawn/effect 中捕获同一副本;set 走内部可变(&self)。 - let status = use_signal(|| Option::::None); + let mut status = use_signal(|| Option::::None); let mut loading = use_signal(|| true); - let error = use_signal(|| Option::::None); + let mut error = use_signal(|| Option::::None); // 自动刷新间隔(秒);None = 手动。DB 查询有成本,最低 1s。 let mut refresh_interval: Signal> = use_signal(|| None); @@ -155,9 +158,9 @@ fn DbStatusTab() -> Element { use_future(move || { // 在依赖闭包里读 interval,使 use_future 在它变化时重新执行。 let interval_secs = refresh_interval(); - let status_f = status; - let loading_f = loading; - let error_f = error; + let mut status_f = status; + let mut loading_f = loading; + let mut error_f = error; async move { #[cfg(target_arch = "wasm32")] { @@ -397,15 +400,16 @@ fn format_uptime(secs: u64) -> String { /// 手动刷新 + 自动刷新开关(500ms/1s/2s/5s/手动,默认手动)。 /// 主机层数据由后台 500ms 采样,前端轮询只读快照零成本,故可高频。 #[allow(non_snake_case)] +#[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut, unused_variables))] fn ServerStatusTab() -> Element { use crate::api::database::system_status::ServerStatus; #[cfg(target_arch = "wasm32")] use crate::api::database::system_status::get_server_status; use crate::components::ui::{ADMIN_CARD_CLASS, ADMIN_TABLE_CLASS}; - let status = use_signal(|| Option::::None); + let mut status = use_signal(|| Option::::None); let mut loading = use_signal(|| true); - let error = use_signal(|| Option::::None); + let mut error = use_signal(|| Option::::None); // 自动刷新间隔(毫秒);None = 手动。主机层后台采样,前端可高频轮询。 let mut refresh_ms: Signal> = use_signal(|| None); @@ -437,9 +441,9 @@ fn ServerStatusTab() -> Element { // 自动刷新:refresh_ms 变化时重建 future。 use_future(move || { let interval_ms = refresh_ms(); - let status_f = status; - let loading_f = loading; - let error_f = error; + let mut status_f = status; + let mut loading_f = loading; + let mut error_f = error; async move { #[cfg(target_arch = "wasm32")] { @@ -639,6 +643,7 @@ fn ServerStatusTab() -> Element { /// /// 护栏 4(前端二次确认):提交写操作前弹窗确认。 #[allow(non_snake_case)] +#[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut, unused_variables))] fn SqlConsoleTab() -> Element { use crate::api::database::sql_console::SqlResult; #[cfg(target_arch = "wasm32")] @@ -654,7 +659,7 @@ fn SqlConsoleTab() -> Element { let theme = use_theme(); let sql_text = use_signal(String::new); - let result = use_signal(|| Option::::None); + let mut result = use_signal(|| Option::::None); let mut error = use_signal(|| Option::::None); let mut running = use_signal(|| false); // 选项 toggles @@ -672,9 +677,9 @@ fn SqlConsoleTab() -> Element { // 初始化 CodeMirror + 拉取 schema 注入补全。仅 WASM。 #[cfg(target_arch = "wasm32")] { - use dioxus::prelude::wasm_bindgen::closure::Closure; + use wasm_bindgen::closure::Closure; use_effect(move || { - if editor_handle().is_some() { + if editor_handle.read().is_some() { return; } let mut text = sql_text; @@ -702,8 +707,12 @@ fn SqlConsoleTab() -> Element { // 异步拉取 schema 注入补全 spawn(async move { if let Ok(schema) = get_db_schema().await { - if let Some(h) = editor_handle.read().as_ref() { - h.instance().set_schema(&schema); + // SqlSchema 是 serde 类型:先用 serde_wasm_bindgen 转 JsValue, + // 再传给 CodeMirror(extern set_schema 只接受 &JsValue)。 + if let Ok(js) = serde_wasm_bindgen::to_value(&schema) { + if let Some(h) = editor_handle.read().as_ref() { + h.instance().set_schema(&js); + } } } }); @@ -1040,6 +1049,7 @@ fn urlencode(s: &str) -> String { /// 备份恢复 tab:备份按钮 + 进度轮询 + 备份列表(下载/恢复/删除)。 #[allow(non_snake_case)] +#[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut, unused_variables))] fn BackupTab() -> Element { use crate::api::database::backup::BackupInfo; use crate::api::database::tasks::TaskProgress; @@ -1049,23 +1059,25 @@ fn BackupTab() -> Element { use crate::api::database::tasks::{get_task_progress, TaskStatus}; use crate::components::ui::{ADMIN_CARD_CLASS, ADMIN_TABLE_CLASS}; + // backups/active_task_id 仅在闭包内的重绑定副本上 .set()(如 backups_f), + // 外层绑定本身不改值,故无需 mut。 let backups = use_signal(Vec::::new); let mut loading = use_signal(|| false); - let error = use_signal(|| Option::::None); + let mut error = use_signal(|| Option::::None); // 当前进行中的任务(备份/恢复)id + 进度 let active_task_id: Signal> = use_signal(|| None); - let active_progress = use_signal(|| Option::::None); + let mut active_progress = use_signal(|| Option::::None); // 待恢复的文件名(确认对话框用) let mut pending_restore: Signal> = use_signal(|| None); - let busy = use_signal(|| false); + let mut busy = use_signal(|| false); // 刷新备份列表 let mut refresh_list = move || { loading.set(true); #[cfg(target_arch = "wasm32")] { - let backups = backups; - let error = error; + let mut backups = backups; + let mut error = error; spawn(async move { match list_backups().await { Ok(list) => { @@ -1089,17 +1101,19 @@ fn BackupTab() -> Element { // 恢复确认:pending_restore 被设置时弹出原生 confirm,确认后发起 restore_backup // 并开始进度轮询。用 use_future 响应 pending_restore 变化。 + // _pending_for_confirm 被 clone 进 async move(FnMut 可重入),用 Option::clone 兜底。 let _pending_for_confirm = pending_restore.read().clone(); use_future(move || { - let pending_restore = pending_restore; - let busy = busy; - let active_progress = active_progress; - let active_task_id = active_task_id; - let error = error; + let mut pending_restore = pending_restore; + let mut busy = busy; + let mut active_progress = active_progress; + let mut active_task_id = active_task_id; + let mut error = error; + let pending_for_confirm = _pending_for_confirm.clone(); async move { #[cfg(target_arch = "wasm32")] { - let fname = match _pending_for_confirm { + let fname = match pending_for_confirm { Some(f) => f, None => return, }; @@ -1128,22 +1142,24 @@ fn BackupTab() -> Element { } #[cfg(not(target_arch = "wasm32"))] { - let _ = (pending_restore, busy, active_progress, active_task_id, error); + let _ = (pending_restore, busy, active_progress, active_task_id, error, pending_for_confirm); } } }); // 任务进度轮询:active_task_id 存在时每 1.5s 拉取进度,Done/Failed 后停止 + 刷新列表 + // _task_id_for_poll 被 clone 进 async move(FnMut 可重入)。 let _task_id_for_poll = active_task_id.read().clone(); use_future(move || { - let active_task_id = active_task_id; - let active_progress = active_progress; - let backups_f = backups; - let busy_f = busy; + let mut active_task_id = active_task_id; + let mut active_progress = active_progress; + let mut backups_f = backups; + let mut busy_f = busy; + let task_id_for_poll = _task_id_for_poll.clone(); async move { #[cfg(target_arch = "wasm32")] { - let tid = match _task_id_for_poll { + let tid = match task_id_for_poll { Some(t) => t, None => return, }; @@ -1174,7 +1190,7 @@ fn BackupTab() -> Element { } #[cfg(not(target_arch = "wasm32"))] { - let _ = (active_task_id, active_progress, backups_f, busy_f); + let _ = (active_task_id, active_progress, backups_f, busy_f, task_id_for_poll); } } }); @@ -1209,7 +1225,7 @@ fn BackupTab() -> Element { { busy.set(true); active_progress.set(None); - let active_task_id = active_task_id; + let mut active_task_id = active_task_id; spawn(async move { match create_backup().await { Ok(id) => active_task_id.set(Some(id)), @@ -1276,8 +1292,8 @@ fn BackupTab() -> Element { size_str: size_str.clone(), dl_url: dl_url.clone(), busy: is_busy, - on_restore: move |f| pending_restore.set(Some(f)), - on_delete: move |_fname_del| { + on_restore: move |f: String| pending_restore.set(Some(f)), + on_delete: move |_fname_del: String| { #[cfg(target_arch = "wasm32")] { let fname_del = _fname_del; @@ -1288,7 +1304,7 @@ fn BackupTab() -> Element { }) == Some(true); if confirmed { - let backups = backups; + let mut backups = backups; spawn(async move { let _ = delete_backup(fname_del.clone()).await; if let Ok(list) = list_backups().await { diff --git a/src/sysinfo_sampler.rs b/src/sysinfo_sampler.rs index 93c37a5..f0ad772 100644 --- a/src/sysinfo_sampler.rs +++ b/src/sysinfo_sampler.rs @@ -5,7 +5,10 @@ //! //! 采样间隔由环境变量 `SYSINFO_SAMPLE_SECS` 配置(默认 0.5 秒,支持小数如 0.1)。 +// LazyLock / Duration 仅 server 构建的采样任务用到;WASM 端只序列化 SystemSnapshot。 +#[cfg(feature = "server")] use std::sync::LazyLock; +#[cfg(feature = "server")] use std::time::Duration; use serde::{Deserialize, Serialize};