@@ -285,6 +285,70 @@ pub fn monitoring_age_secs() -> Option<i64> {
285285 Storage :: open ( & path) . ok ( ) . and_then ( |s| s. monitoring_age_secs ( ) )
286286}
287287
288+ /// Read the most-recent DEEP-VITALS sample of each surface for `server` out of
289+ /// the sentinel store, shaped for the live UI's "DEEP VITALS" panel.
290+ ///
291+ /// Honest empty state, never an error: if the store doesn't exist yet, or the
292+ /// server has never been monitored (no instance row), every surface is `null`
293+ /// (I/O latency `[]`) and `has_data` is `false`. `captured_at` is the newest
294+ /// instant across all surfaces present (epoch millis), or `null` when empty —
295+ /// the UI shows it as "as of …".
296+ pub fn deep_vitals ( server : & str ) -> serde_json:: Value {
297+ let empty = || {
298+ serde_json:: json!( {
299+ "has_data" : false ,
300+ "captured_at" : serde_json:: Value :: Null ,
301+ "cpu_pressure" : serde_json:: Value :: Null ,
302+ "memory_headroom" : serde_json:: Value :: Null ,
303+ "io_latency" : [ ] ,
304+ "tempdb_contention" : serde_json:: Value :: Null ,
305+ "plan_cache" : serde_json:: Value :: Null ,
306+ } )
307+ } ;
308+
309+ let path = SentinelConfig :: default_db_path ( ) ;
310+ let storage = match Storage :: open ( & path) {
311+ Ok ( s) => s,
312+ Err ( _) => return empty ( ) , // store not created yet → not an error
313+ } ;
314+ let Some ( instance_id) = storage. get_instance_id ( server) else {
315+ return empty ( ) ; // server never monitored → not an error
316+ } ;
317+
318+ let cpu = storage. latest_cpu_pressure ( instance_id) ;
319+ let mem = storage. latest_memory_headroom ( instance_id) ;
320+ let io = storage. latest_io_latency ( instance_id) ;
321+ let tempdb = storage. latest_tempdb_contention ( instance_id) ;
322+ let plan = storage. latest_plan_cache ( instance_id) ;
323+
324+ // Newest captured_at across whichever surfaces have data (millis).
325+ let captured_at_ms = [
326+ cpu. as_ref ( ) . map ( |r| r. captured_at . timestamp_millis ( ) ) ,
327+ mem. as_ref ( ) . map ( |r| r. captured_at . timestamp_millis ( ) ) ,
328+ io. first ( ) . map ( |r| r. captured_at . timestamp_millis ( ) ) ,
329+ tempdb. as_ref ( ) . map ( |r| r. captured_at . timestamp_millis ( ) ) ,
330+ plan. as_ref ( ) . map ( |r| r. captured_at . timestamp_millis ( ) ) ,
331+ ]
332+ . into_iter ( )
333+ . flatten ( )
334+ . max ( ) ;
335+
336+ let has_data =
337+ cpu. is_some ( ) || mem. is_some ( ) || !io. is_empty ( ) || tempdb. is_some ( ) || plan. is_some ( ) ;
338+
339+ // The row structs derive Serialize, so each maps straight to JSON; the field
340+ // names match the storage row structs (snake_case), which the UI consumes.
341+ serde_json:: json!( {
342+ "has_data" : has_data,
343+ "captured_at" : captured_at_ms,
344+ "cpu_pressure" : cpu,
345+ "memory_headroom" : mem,
346+ "io_latency" : io,
347+ "tempdb_contention" : tempdb,
348+ "plan_cache" : plan,
349+ } )
350+ }
351+
288352pub fn build_report ( window : TimeRange ) -> WeeklyReport {
289353 let path = SentinelConfig :: default_db_path ( ) ;
290354 match Storage :: open ( & path) {
0 commit comments