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.
213 lines
7.4 KiB
Rust
213 lines
7.4 KiB
Rust
#![allow(clippy::unused_unit, deprecated)]
|
||
|
||
//! 数据库运行状态聚合查询(只读)。
|
||
//!
|
||
//! 全部查询走 `pg_catalog` / `pg_stat_*` / `schema_migrations`,零写、零风险。
|
||
//! [`get_db_status`] 在一次 server function 调用里聚合多组数据返回。
|
||
|
||
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;
|
||
|
||
/// 数据库状态聚合数据。
|
||
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
||
pub struct DbStatus {
|
||
/// 当前数据库总大小(字节)。
|
||
pub db_size_bytes: i64,
|
||
/// 当前数据库的活跃连接数。
|
||
pub total_connections: i32,
|
||
/// PG 配置的最大连接数(`max_connections`)。
|
||
pub max_connections: i32,
|
||
/// 已应用的最新迁移版本(`schema_migrations.version`)。
|
||
pub migration_version: Option<String>,
|
||
/// 最新迁移的应用时间。
|
||
pub migration_applied_at: Option<DateTime<Utc>>,
|
||
/// 用户表清单(按总大小降序)。
|
||
pub tables: Vec<TableInfo>,
|
||
/// 索引占用 Top N。
|
||
pub top_indexes: Vec<IndexInfo>,
|
||
/// 活跃连接列表(已过滤掉自身这条查询)。
|
||
pub active_connections: Vec<ConnInfo>,
|
||
}
|
||
|
||
/// 单张表的统计信息。
|
||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||
pub struct TableInfo {
|
||
pub name: String,
|
||
/// 行数估算(`pg_class.reltuples`,非 COUNT(*),UI 标注"~估算")。
|
||
pub row_estimate: i64,
|
||
pub table_size_bytes: i64,
|
||
pub index_size_bytes: i64,
|
||
pub total_size_bytes: i64,
|
||
pub last_vacuum: Option<DateTime<Utc>>,
|
||
pub last_analyze: Option<DateTime<Utc>>,
|
||
pub dead_tuples: i64,
|
||
pub live_tuples: i64,
|
||
}
|
||
|
||
/// 索引占用信息。
|
||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||
pub struct IndexInfo {
|
||
pub name: String,
|
||
pub table_name: String,
|
||
pub size_bytes: i64,
|
||
}
|
||
|
||
/// 单条活跃连接信息。
|
||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||
pub struct ConnInfo {
|
||
pub pid: i32,
|
||
pub user: String,
|
||
pub state: Option<String>,
|
||
pub query: Option<String>,
|
||
/// 当前查询已运行秒数(无查询时为 None)。
|
||
pub query_duration_secs: Option<f64>,
|
||
}
|
||
|
||
/// 获取数据库运行状态(只读,管理员)。
|
||
#[server(GetDbStatus, "/api")]
|
||
pub async fn get_db_status() -> Result<DbStatus, ServerFnError> {
|
||
let _user = get_current_admin_user().await?;
|
||
|
||
#[cfg(feature = "server")]
|
||
{
|
||
let client = get_conn().await.map_err(AppError::db_conn)?;
|
||
|
||
// 数据库总大小
|
||
let db_size: i64 = client
|
||
.query_one("SELECT pg_database_size(current_database())", &[])
|
||
.await
|
||
.map_err(AppError::query)?
|
||
.get(0);
|
||
|
||
// 当前库连接数 + 全局最大连接数
|
||
let conn_row = client
|
||
.query_one(
|
||
"SELECT count(*), \
|
||
(SELECT setting::int FROM pg_settings WHERE name = 'max_connections') \
|
||
FROM pg_stat_activity WHERE datname = current_database()",
|
||
&[],
|
||
)
|
||
.await
|
||
.map_err(AppError::query)?;
|
||
let total_conn: i32 = conn_row.get(0);
|
||
let max_conn: i32 = conn_row.get(1);
|
||
|
||
// 最新迁移版本(schema_migrations 由 migrate.rs 创建)
|
||
let migration = client
|
||
.query_opt(
|
||
"SELECT version, applied_at FROM schema_migrations \
|
||
ORDER BY applied_at DESC LIMIT 1",
|
||
&[],
|
||
)
|
||
.await
|
||
.map_err(AppError::query)?;
|
||
let (migration_version, migration_applied_at) = match migration {
|
||
Some(row) => (Some(row.get(0)), Some(row.get(1))),
|
||
None => (None, None),
|
||
};
|
||
|
||
// 表清单:行数用 reltuples 估算,避免大表 COUNT(*) 拖垮库
|
||
let table_rows = client
|
||
.query(
|
||
"SELECT c.relname, c.reltuples::bigint, pg_relation_size(c.oid), \
|
||
pg_total_relation_size(c.oid) - pg_relation_size(c.oid), \
|
||
pg_total_relation_size(c.oid), s.last_vacuum, s.last_analyze, \
|
||
s.n_dead_tup, s.n_live_tup \
|
||
FROM pg_class c \
|
||
JOIN pg_namespace n ON n.oid = c.relnamespace \
|
||
LEFT JOIN pg_stat_user_tables s ON s.relid = c.oid \
|
||
WHERE c.relkind = 'r' AND n.nspname = 'public' \
|
||
ORDER BY pg_total_relation_size(c.oid) DESC",
|
||
&[],
|
||
)
|
||
.await
|
||
.map_err(AppError::query)?;
|
||
let tables = table_rows
|
||
.into_iter()
|
||
.map(|r| TableInfo {
|
||
name: r.get(0),
|
||
row_estimate: r.get(1),
|
||
table_size_bytes: r.get(2),
|
||
index_size_bytes: r.get(3),
|
||
total_size_bytes: r.get(4),
|
||
last_vacuum: r.get(5),
|
||
last_analyze: r.get(6),
|
||
dead_tuples: r.get(7),
|
||
live_tuples: r.get(8),
|
||
})
|
||
.collect();
|
||
|
||
// 索引占用 Top 10
|
||
let index_rows = client
|
||
.query(
|
||
"SELECT c.relname AS index_name, cl.relname AS table_name, \
|
||
pg_relation_size(c.oid) \
|
||
FROM pg_class c \
|
||
JOIN pg_index i ON i.indexrelid = c.oid \
|
||
JOIN pg_class cl ON cl.oid = i.indrelid \
|
||
JOIN pg_namespace n ON n.oid = cl.relnamespace \
|
||
WHERE n.nspname = 'public' \
|
||
ORDER BY pg_relation_size(c.oid) DESC LIMIT 10",
|
||
&[],
|
||
)
|
||
.await
|
||
.map_err(AppError::query)?;
|
||
let top_indexes = index_rows
|
||
.into_iter()
|
||
.map(|r| IndexInfo {
|
||
name: r.get(0),
|
||
table_name: r.get(1),
|
||
size_bytes: r.get(2),
|
||
})
|
||
.collect();
|
||
|
||
// 活跃连接(过滤自身 pid,避免循环显示)
|
||
let conn_rows = client
|
||
.query(
|
||
"SELECT pid, usename, state, query, \
|
||
extract(epoch FROM now() - query_start) \
|
||
FROM pg_stat_activity \
|
||
WHERE datname = current_database() AND pid <> pg_backend_pid() \
|
||
ORDER BY query_start DESC NULLS LAST LIMIT 50",
|
||
&[],
|
||
)
|
||
.await
|
||
.map_err(AppError::query)?;
|
||
let active_connections = conn_rows
|
||
.into_iter()
|
||
.map(|r| ConnInfo {
|
||
pid: r.get(0),
|
||
user: r.get::<_, Option<String>>(1).unwrap_or_default(),
|
||
state: r.get(2),
|
||
query: r.get(3),
|
||
query_duration_secs: r.get(4),
|
||
})
|
||
.collect();
|
||
|
||
Ok(DbStatus {
|
||
db_size_bytes: db_size,
|
||
total_connections: total_conn,
|
||
max_connections: max_conn,
|
||
migration_version,
|
||
migration_applied_at,
|
||
tables,
|
||
top_indexes,
|
||
active_connections,
|
||
})
|
||
}
|
||
#[cfg(not(feature = "server"))]
|
||
{
|
||
Ok(DbStatus::default())
|
||
}
|
||
}
|