1#![allow(clippy::unused_unit, deprecated)]
2
3use crate::api::error::ApiError;
9use chrono::{DateTime, Utc};
10use leptos::prelude::*;
11use serde::{Deserialize, Serialize};
12
13#[cfg(feature = "server")]
16use crate::api::auth::get_current_admin_user;
17#[cfg(feature = "server")]
18use crate::api::error::AppError;
19#[cfg(feature = "server")]
20use crate::db::pool::get_conn;
21
22#[derive(Serialize, Deserialize, Debug, Default, Clone)]
24pub struct DbStatus {
25 pub db_size_bytes: i64,
27 pub total_connections: i32,
29 pub max_connections: i32,
31 pub migration_version: Option<String>,
33 #[cfg_attr(
35 target_arch = "wasm32",
36 serde(
37 default,
38 deserialize_with = "crate::models::rfc3339::deserialize_option"
39 )
40 )]
41 pub migration_applied_at: Option<DateTime<Utc>>,
42 pub tables: Vec<TableInfo>,
44 pub top_indexes: Vec<IndexInfo>,
46 pub active_connections: Vec<ConnInfo>,
48}
49
50#[derive(Serialize, Deserialize, Debug, Clone)]
52pub struct TableInfo {
53 pub name: String,
54 pub row_count: i64,
56 pub row_count_estimated: bool,
58 pub table_size_bytes: i64,
59 pub index_size_bytes: i64,
60 pub total_size_bytes: i64,
61 #[cfg_attr(
62 target_arch = "wasm32",
63 serde(
64 default,
65 deserialize_with = "crate::models::rfc3339::deserialize_option"
66 )
67 )]
68 pub last_vacuum: Option<DateTime<Utc>>,
69 #[cfg_attr(
70 target_arch = "wasm32",
71 serde(
72 default,
73 deserialize_with = "crate::models::rfc3339::deserialize_option"
74 )
75 )]
76 pub last_analyze: Option<DateTime<Utc>>,
77 pub dead_tuples: i64,
78 pub live_tuples: i64,
79}
80
81#[derive(Serialize, Deserialize, Debug, Clone)]
83pub struct IndexInfo {
84 pub name: String,
85 pub table_name: String,
86 pub size_bytes: i64,
87}
88
89#[derive(Serialize, Deserialize, Debug, Clone)]
91pub struct ConnInfo {
92 pub pid: i32,
93 pub user: String,
94 pub state: Option<String>,
95 pub query: Option<String>,
96 pub query_duration_secs: Option<f64>,
98}
99
100#[server(GetDbStatus, endpoint = "get_db_status", input = leptos::server_fn::codec::Json)]
102pub async fn get_db_status() -> Result<DbStatus, ApiError> {
103 let _user = get_current_admin_user().await?;
104
105 #[cfg(feature = "server")]
106 {
107 let client = get_conn().await.map_err(AppError::db_conn)?;
108
109 let db_size: i64 = client
111 .query_one("SELECT pg_database_size(current_database())", &[])
112 .await
113 .map_err(AppError::query)?
114 .get(0);
115
116 let conn_row = client
120 .query_one(
121 "SELECT count(*)::int, \
122 (SELECT setting::int FROM pg_settings WHERE name = 'max_connections') \
123 FROM pg_stat_activity WHERE datname = current_database()",
124 &[],
125 )
126 .await
127 .map_err(AppError::query)?;
128 let total_conn: i32 = conn_row.get(0);
129 let max_conn: i32 = conn_row.get(1);
130
131 let migration = client
133 .query_opt(
134 "SELECT version, applied_at FROM schema_migrations \
135 ORDER BY applied_at DESC LIMIT 1",
136 &[],
137 )
138 .await
139 .map_err(AppError::query)?;
140 let (migration_version, migration_applied_at) = match migration {
141 Some(row) => (Some(row.get(0)), Some(row.get(1))),
142 None => (None, None),
143 };
144
145 const COUNT_SIZE_THRESHOLD: i64 = 100 * 1024 * 1024;
148 let table_rows = client
149 .query(
150 "SELECT c.relname, c.reltuples::bigint, pg_relation_size(c.oid), \
151 pg_total_relation_size(c.oid) - pg_relation_size(c.oid), \
152 pg_total_relation_size(c.oid), s.last_vacuum, s.last_analyze, \
153 s.n_dead_tup, s.n_live_tup \
154 FROM pg_class c \
155 JOIN pg_namespace n ON n.oid = c.relnamespace \
156 LEFT JOIN pg_stat_user_tables s ON s.relid = c.oid \
157 WHERE c.relkind = 'r' AND n.nspname = 'public' \
158 ORDER BY pg_total_relation_size(c.oid) DESC",
159 &[],
160 )
161 .await
162 .map_err(AppError::query)?;
163 let tables = {
164 let mut out = Vec::with_capacity(table_rows.len());
165 for r in table_rows {
166 let name: String = r.get(0);
167 let reltuples: i64 = r.get(1);
168 let total_size: i64 = r.get(4);
169 let (row_count, estimated) = if total_size < COUNT_SIZE_THRESHOLD {
172 let ident = format!("\"{}\"", name.replace('"', "\"\""));
173 let cnt: i64 = client
174 .query_one(&format!("SELECT count(*)::bigint FROM {}", ident), &[])
175 .await
176 .map_err(AppError::query)?
177 .get(0);
178 (cnt, false)
179 } else {
180 (reltuples, true)
181 };
182 out.push(TableInfo {
183 name,
184 row_count,
185 row_count_estimated: estimated,
186 table_size_bytes: r.get(2),
187 index_size_bytes: r.get(3),
188 total_size_bytes: total_size,
189 last_vacuum: r.get(5),
190 last_analyze: r.get(6),
191 dead_tuples: r.get(7),
192 live_tuples: r.get(8),
193 });
194 }
195 out
196 };
197
198 let index_rows = client
200 .query(
201 "SELECT c.relname AS index_name, cl.relname AS table_name, \
202 pg_relation_size(c.oid) \
203 FROM pg_class c \
204 JOIN pg_index i ON i.indexrelid = c.oid \
205 JOIN pg_class cl ON cl.oid = i.indrelid \
206 JOIN pg_namespace n ON n.oid = cl.relnamespace \
207 WHERE n.nspname = 'public' \
208 ORDER BY pg_relation_size(c.oid) DESC LIMIT 10",
209 &[],
210 )
211 .await
212 .map_err(AppError::query)?;
213 let top_indexes = index_rows
214 .into_iter()
215 .map(|r| IndexInfo {
216 name: r.get(0),
217 table_name: r.get(1),
218 size_bytes: r.get(2),
219 })
220 .collect();
221
222 let conn_rows = client
226 .query(
227 "SELECT pid, usename, state, query, \
228 extract(epoch FROM now() - query_start)::double precision \
229 FROM pg_stat_activity \
230 WHERE datname = current_database() AND pid <> pg_backend_pid() \
231 ORDER BY query_start DESC NULLS LAST LIMIT 50",
232 &[],
233 )
234 .await
235 .map_err(AppError::query)?;
236 let active_connections = conn_rows
237 .into_iter()
238 .map(|r| ConnInfo {
239 pid: r.get(0),
240 user: r.get::<_, Option<String>>(1).unwrap_or_default(),
241 state: r.get(2),
242 query: r.get(3),
243 query_duration_secs: r.get(4),
244 })
245 .collect();
246
247 Ok(DbStatus {
248 db_size_bytes: db_size,
249 total_connections: total_conn,
250 max_connections: max_conn,
251 migration_version,
252 migration_applied_at,
253 tables,
254 top_indexes,
255 active_connections,
256 })
257 }
258 #[cfg(not(feature = "server"))]
259 {
260 Ok(DbStatus::default())
261 }
262}