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